@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
|
@@ -29,6 +29,10 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
29
29
|
*/
|
|
30
30
|
interface RouterWithRouteMap {
|
|
31
31
|
routeMap: Record<string, unknown>;
|
|
32
|
+
// findMatch is async (it may await an async include provider's import before
|
|
33
|
+
// the route's names are spliced into routeMap), so expandLazyIncludes awaits
|
|
34
|
+
// it. Typed `unknown` (not `unknown | Promise<unknown>`, which collapses to
|
|
35
|
+
// the same thing) — the awaited value is never read, only its side effect.
|
|
32
36
|
findMatch?: (pathname: string) => unknown;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -42,16 +46,28 @@ function concretePath(pattern: string): string {
|
|
|
42
46
|
);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function expandLazyIncludes(
|
|
49
|
+
async function expandLazyIncludes(
|
|
46
50
|
router: RouterWithRouteMap,
|
|
47
51
|
patterns: Iterable<string>,
|
|
48
|
-
): void {
|
|
52
|
+
): Promise<void> {
|
|
49
53
|
const findMatch = router.findMatch;
|
|
50
54
|
if (typeof findMatch !== "function") return;
|
|
51
55
|
for (const pattern of patterns) {
|
|
52
56
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
// Await: an async include(`() => import()`) resolves its module + splices
|
|
58
|
+
// its routes into routeMap only after findMatch's Promise settles. Without
|
|
59
|
+
// the await, the routeMap read below races the import and reports the
|
|
60
|
+
// split-group routes as missing.
|
|
61
|
+
await findMatch.call(router, concretePath(pattern));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// A findMatch throw here is almost always a failing async include()
|
|
64
|
+
// provider (its `() => import()` rejected). Surface it — swallowed, it
|
|
65
|
+
// resurfaces downstream as a misleading "missing route" in the diff.
|
|
66
|
+
console.warn(
|
|
67
|
+
`[@rangojs/router] assertGeneratedRoutesMatch: could not expand include ` +
|
|
68
|
+
`for "${pattern}": ${(e as Error)?.message ?? String(e)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
}
|
|
57
73
|
|
|
@@ -91,13 +107,13 @@ function patternOf(value: unknown): string {
|
|
|
91
107
|
return String(value);
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
export function diffGeneratedRoutes(
|
|
110
|
+
export async function diffGeneratedRoutes(
|
|
95
111
|
router: RouterWithRouteMap,
|
|
96
112
|
generatedMap?: Record<string, unknown>,
|
|
97
|
-
): GeneratedRoutesDiff {
|
|
113
|
+
): Promise<GeneratedRoutesDiff> {
|
|
98
114
|
const generated = generatedMap ?? getGlobalRouteMap();
|
|
99
115
|
|
|
100
|
-
expandLazyIncludes(
|
|
116
|
+
await expandLazyIncludes(
|
|
101
117
|
router,
|
|
102
118
|
Object.values(generated).map((v) => patternOf(v)),
|
|
103
119
|
);
|
|
@@ -145,14 +161,14 @@ export function diffGeneratedRoutes(
|
|
|
145
161
|
* import generated from "./router.named-routes.gen";
|
|
146
162
|
* import { router } from "./router";
|
|
147
163
|
*
|
|
148
|
-
* assertGeneratedRoutesMatch(router, generated);
|
|
164
|
+
* await assertGeneratedRoutesMatch(router, generated);
|
|
149
165
|
* ```
|
|
150
166
|
*/
|
|
151
|
-
export function assertGeneratedRoutesMatch(
|
|
167
|
+
export async function assertGeneratedRoutesMatch(
|
|
152
168
|
router: RouterWithRouteMap,
|
|
153
169
|
generatedMap?: Record<string, unknown>,
|
|
154
|
-
): void {
|
|
155
|
-
const diff = diffGeneratedRoutes(router, generatedMap);
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const diff = await diffGeneratedRoutes(router, generatedMap);
|
|
156
172
|
if (diff.ok) return;
|
|
157
173
|
|
|
158
174
|
const lines: string[] = [
|
package/src/testing/index.ts
CHANGED
|
@@ -47,6 +47,12 @@ export type {
|
|
|
47
47
|
TestLoaderContext,
|
|
48
48
|
} from "./run-loader.js";
|
|
49
49
|
|
|
50
|
+
export { runTransitionWhen } from "./run-transition-when.js";
|
|
51
|
+
export type {
|
|
52
|
+
RunTransitionWhenOptions,
|
|
53
|
+
RunTransitionWhenResult,
|
|
54
|
+
} from "./run-transition-when.js";
|
|
55
|
+
|
|
50
56
|
export { dispatch } from "./dispatch.js";
|
|
51
57
|
export type { DispatchOptions } from "./dispatch.js";
|
|
52
58
|
|
|
@@ -116,6 +116,15 @@ export interface RenderHandlerOptions<TEnv = any> {
|
|
|
116
116
|
* `"use cache: profileName"` resolution once a `cacheStore` is wired.
|
|
117
117
|
*/
|
|
118
118
|
cacheProfiles?: Record<string, CacheProfile>;
|
|
119
|
+
/**
|
|
120
|
+
* Render as if inside a server action's revalidation render (production sets
|
|
121
|
+
* this in revalidateAfterAction). A stale `"use cache"` entry whose profile
|
|
122
|
+
* opts into `foregroundOnAction` then re-executes in the FOREGROUND (fresh
|
|
123
|
+
* result in this render) instead of being served stale + revalidated in the
|
|
124
|
+
* background. Without it, a stale entry keeps SWR. Pair with `cacheStore` +
|
|
125
|
+
* `cacheProfiles` to exercise the `foregroundOnAction` opt-in.
|
|
126
|
+
*/
|
|
127
|
+
inActionRevalidation?: boolean;
|
|
119
128
|
/**
|
|
120
129
|
* Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
|
|
121
130
|
* or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert,
|
|
@@ -238,6 +247,11 @@ export async function renderHandler<TEnv = any>(
|
|
|
238
247
|
opts.theme === undefined ? undefined : resolveThemeConfig(opts.theme),
|
|
239
248
|
});
|
|
240
249
|
|
|
250
|
+
// Simulate an action revalidation render (production sets this in
|
|
251
|
+
// revalidateAfterAction) so a `foregroundOnAction` cache profile foregrounds a
|
|
252
|
+
// stale entry. See the foregroundOnAction option doc.
|
|
253
|
+
if (opts.inActionRevalidation) reqCtx._inActionRevalidation = true;
|
|
254
|
+
|
|
241
255
|
const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
|
|
242
256
|
const handlePushes = new Map<Handle<any, any>, unknown[]>();
|
|
243
257
|
|
|
@@ -46,11 +46,15 @@ import {
|
|
|
46
46
|
generateHistoryKey,
|
|
47
47
|
} from "../browser/navigation-store.js";
|
|
48
48
|
import { createEventController } from "../browser/event-controller.js";
|
|
49
|
+
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
49
50
|
import type { NavigationStore, NavigationBridge } from "../browser/types.js";
|
|
50
51
|
import type { EventController } from "../browser/event-controller.js";
|
|
51
52
|
import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
|
|
52
53
|
import { NavigationProvider } from "../browser/react/NavigationProvider.js";
|
|
53
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
compilePattern,
|
|
56
|
+
buildParamsFromMatch,
|
|
57
|
+
} from "../router/pattern-matching.js";
|
|
54
58
|
import { normalizeBasename } from "../router/basename.js";
|
|
55
59
|
import type { LoaderDefinition } from "../types.js";
|
|
56
60
|
import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
|
|
@@ -302,14 +306,9 @@ function matchLeaf(
|
|
|
302
306
|
const compiled = compilePattern(pattern);
|
|
303
307
|
const match = compiled.regex.exec(pathname);
|
|
304
308
|
if (!match) return null;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (value !== undefined) {
|
|
309
|
-
params[name] = decodeURIComponent(value);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
return params;
|
|
309
|
+
// Reuse the production param builder so the harness matches the real matcher
|
|
310
|
+
// exactly (named catch-all "" binding, decoding) instead of forking it.
|
|
311
|
+
return buildParamsFromMatch(match, compiled.paramNames, compiled.catchAll);
|
|
313
312
|
}
|
|
314
313
|
|
|
315
314
|
function staticPrefix(pattern: string): string {
|
|
@@ -481,8 +480,12 @@ export async function renderRoute(
|
|
|
481
480
|
|
|
482
481
|
const eventController = createEventController({ initialLocation: url });
|
|
483
482
|
eventController.setParams(initialMatch.params);
|
|
483
|
+
// Resolve-by-default: resolve any deferred (Promise) seeded handle values
|
|
484
|
+
// before applying, so the seeded handles reach collect/useHandle resolved —
|
|
485
|
+
// matching what the server/client do in a real app.
|
|
486
|
+
const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
|
|
484
487
|
eventController.setHandleData(
|
|
485
|
-
|
|
488
|
+
resolvedSeed,
|
|
486
489
|
initialSegments.map((s) => s.id),
|
|
487
490
|
);
|
|
488
491
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runTransitionWhen — unit-test a transition({ when }) predicate in isolation.
|
|
3
|
+
*
|
|
4
|
+
* Runs the SAME two server functions the router uses — applyViewTransitionDefault
|
|
5
|
+
* (strips the `when` function from the serialized config and records the
|
|
6
|
+
* predicate on the request context) and gateTransitions (assembles the
|
|
7
|
+
* TransitionWhenContext and evaluates the predicate post-handler). So the
|
|
8
|
+
* predicate sees exactly the navigation/action metadata it would at runtime
|
|
9
|
+
* (currentUrl/currentParams/fromRouteName, nextUrl/nextParams/toRouteName,
|
|
10
|
+
* actionId/actionUrl/actionResult/formData/method, get/env), and `kept` reflects
|
|
11
|
+
* whether the transition would apply this request. The result also exposes the
|
|
12
|
+
* assembled `whenContext` so tests can assert the exact fields without reaching
|
|
13
|
+
* into private request-context state.
|
|
14
|
+
*
|
|
15
|
+
* This is the public way to exercise a transition gate: the full
|
|
16
|
+
* match -> render pipeline that wires these together only runs under real RSC
|
|
17
|
+
* rendering (which the Flight primitives do not drive), so without this primitive
|
|
18
|
+
* a consumer could not test their predicate through @rangojs/router/testing.
|
|
19
|
+
*
|
|
20
|
+
* Synchronous: a transition predicate returns a boolean and the gate has no I/O.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
runWithRequestContext,
|
|
25
|
+
type RequestContext,
|
|
26
|
+
} from "../server/request-context.js";
|
|
27
|
+
import { applyViewTransitionDefault } from "../router/segment-resolution/view-transition-default.js";
|
|
28
|
+
import { gateTransitions } from "../rsc/transition-gate.js";
|
|
29
|
+
import { createTestRequestContext, type VarsInit } from "./internal/context.js";
|
|
30
|
+
import type {
|
|
31
|
+
ResolvedSegment,
|
|
32
|
+
TransitionConfig,
|
|
33
|
+
TransitionWhenContext,
|
|
34
|
+
} from "../types/segments.js";
|
|
35
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
36
|
+
|
|
37
|
+
const toURL = (v: string | URL, base: URL): URL =>
|
|
38
|
+
typeof v === "string" ? new URL(v, base.origin) : v;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for runTransitionWhen. All navigation/action fields are optional and
|
|
42
|
+
* default to "absent", matching what the gate sees for an initial full load with
|
|
43
|
+
* no action: omit `currentUrl`/`currentParams`/`fromRouteName` to model the
|
|
44
|
+
* navigation source being unavailable, and omit the `action*` fields to model a
|
|
45
|
+
* plain (non-action) navigation.
|
|
46
|
+
*/
|
|
47
|
+
export interface RunTransitionWhenOptions<TEnv = any> {
|
|
48
|
+
/** The navigation TARGET request (drives `nextUrl`): a Request or URL/path string. Defaults to `http://localhost/`. */
|
|
49
|
+
request?: Request | string;
|
|
50
|
+
/** Route params for the target (`nextParams`). */
|
|
51
|
+
params?: Record<string, string>;
|
|
52
|
+
/** Target route name (`toRouteName`). */
|
|
53
|
+
toRouteName?: string;
|
|
54
|
+
/** Environment bindings surfaced as `env` (and `ctx.env`). */
|
|
55
|
+
env?: TEnv;
|
|
56
|
+
/** Variables a handler/middleware would have set this request, readable via the predicate's `get()`. */
|
|
57
|
+
vars?: VarsInit;
|
|
58
|
+
/** Navigation SOURCE url (`currentUrl`): a URL or path string. */
|
|
59
|
+
currentUrl?: string | URL;
|
|
60
|
+
/** Source route params (`currentParams`). */
|
|
61
|
+
currentParams?: Record<string, string>;
|
|
62
|
+
/** Source route name (`fromRouteName`). */
|
|
63
|
+
fromRouteName?: string;
|
|
64
|
+
/** Id of the action that triggered a revalidation (`actionId`). */
|
|
65
|
+
actionId?: string;
|
|
66
|
+
/** Url the action was submitted from (`actionUrl`). */
|
|
67
|
+
actionUrl?: string | URL;
|
|
68
|
+
/** The action's return value (`actionResult`). */
|
|
69
|
+
actionResult?: unknown;
|
|
70
|
+
/** FormData from a form action (`formData`). */
|
|
71
|
+
formData?: FormData;
|
|
72
|
+
/** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
|
|
73
|
+
onError?: OnErrorCallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Result of runTransitionWhen.
|
|
78
|
+
*/
|
|
79
|
+
export interface RunTransitionWhenResult<TEnv = any> {
|
|
80
|
+
/** True if the transition would apply this request (predicate returned non-false, or there is no `when`). */
|
|
81
|
+
kept: boolean;
|
|
82
|
+
/** Convenience inverse of `kept`. */
|
|
83
|
+
dropped: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The production-assembled predicate context. Undefined when the config has
|
|
86
|
+
* no `when` predicate.
|
|
87
|
+
*/
|
|
88
|
+
whenContext?: TransitionWhenContext<Record<string, string>, TEnv>;
|
|
89
|
+
/** The underlying RequestContext, for additional assertions (`ctx.get(...)`, etc.). */
|
|
90
|
+
ctx: RequestContext<TEnv>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runTransitionWhen<TEnv = any>(
|
|
94
|
+
config: TransitionConfig,
|
|
95
|
+
opts: RunTransitionWhenOptions<TEnv> = {},
|
|
96
|
+
): RunTransitionWhenResult<TEnv> {
|
|
97
|
+
const { ctx } = createTestRequestContext<TEnv>({
|
|
98
|
+
env: opts.env,
|
|
99
|
+
request: opts.request,
|
|
100
|
+
vars: opts.vars,
|
|
101
|
+
params: opts.params,
|
|
102
|
+
});
|
|
103
|
+
const reqCtx = ctx as unknown as RequestContext<TEnv>;
|
|
104
|
+
|
|
105
|
+
// Target route name (the public field the gate reads for `toRouteName`).
|
|
106
|
+
if (opts.toRouteName !== undefined)
|
|
107
|
+
reqCtx.routeName = opts.toRouteName as RequestContext<TEnv>["routeName"];
|
|
108
|
+
// Source (match-time) data the gate reads for currentUrl/currentParams/fromRouteName.
|
|
109
|
+
if (opts.currentUrl !== undefined)
|
|
110
|
+
reqCtx._gateCurrentUrl = toURL(opts.currentUrl, reqCtx.url);
|
|
111
|
+
if (opts.currentParams !== undefined)
|
|
112
|
+
reqCtx._gateCurrentParams = opts.currentParams;
|
|
113
|
+
if (opts.fromRouteName !== undefined)
|
|
114
|
+
reqCtx._prevRouteKey = opts.fromRouteName;
|
|
115
|
+
// Action data the gate reads at the action-bearing call sites.
|
|
116
|
+
if (opts.actionId !== undefined) reqCtx._gateActionId = opts.actionId;
|
|
117
|
+
if (opts.actionUrl !== undefined)
|
|
118
|
+
reqCtx._gateActionUrl = toURL(opts.actionUrl, reqCtx.url);
|
|
119
|
+
if (opts.actionResult !== undefined)
|
|
120
|
+
reqCtx._gateActionResult = opts.actionResult;
|
|
121
|
+
if (opts.formData !== undefined) reqCtx._gateFormData = opts.formData;
|
|
122
|
+
|
|
123
|
+
let whenContext:
|
|
124
|
+
| TransitionWhenContext<Record<string, string>, TEnv>
|
|
125
|
+
| undefined;
|
|
126
|
+
const when = config.when;
|
|
127
|
+
const configForGate: TransitionConfig = when
|
|
128
|
+
? {
|
|
129
|
+
...config,
|
|
130
|
+
when: (c) => {
|
|
131
|
+
whenContext = c as TransitionWhenContext<
|
|
132
|
+
Record<string, string>,
|
|
133
|
+
TEnv
|
|
134
|
+
>;
|
|
135
|
+
return when(c);
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: config;
|
|
139
|
+
|
|
140
|
+
return runWithRequestContext(reqCtx, () => {
|
|
141
|
+
// The real resolution-time collection + post-handler gate, so the predicate
|
|
142
|
+
// sees the production-assembled TransitionWhenContext.
|
|
143
|
+
const serialized = applyViewTransitionDefault(
|
|
144
|
+
configForGate,
|
|
145
|
+
undefined,
|
|
146
|
+
"tx-when-seg",
|
|
147
|
+
);
|
|
148
|
+
const segment = {
|
|
149
|
+
id: "tx-when-seg",
|
|
150
|
+
namespace: "r",
|
|
151
|
+
type: "route",
|
|
152
|
+
index: 0,
|
|
153
|
+
component: null,
|
|
154
|
+
transition: serialized,
|
|
155
|
+
} as ResolvedSegment;
|
|
156
|
+
gateTransitions(
|
|
157
|
+
[segment],
|
|
158
|
+
reqCtx as Parameters<typeof gateTransitions>[1],
|
|
159
|
+
opts.onError,
|
|
160
|
+
);
|
|
161
|
+
const kept = segment.transition !== undefined;
|
|
162
|
+
return { kept, dropped: !kept, whenContext, ctx: reqCtx };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -109,27 +109,6 @@ function applyThemeToDocument(theme: Theme, config: ResolvedThemeConfig): void {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function getStoredTheme(config: ResolvedThemeConfig): Theme {
|
|
113
|
-
const { storageKey, themes, defaultTheme, enableSystem } = config;
|
|
114
|
-
|
|
115
|
-
let stored = readThemeFromCookie(storageKey);
|
|
116
|
-
|
|
117
|
-
if (!stored) {
|
|
118
|
-
stored = readThemeFromStorage(storageKey);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (stored) {
|
|
122
|
-
if (stored === "system" && enableSystem) {
|
|
123
|
-
return "system";
|
|
124
|
-
}
|
|
125
|
-
if (themes.includes(stored)) {
|
|
126
|
-
return stored as Theme;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return defaultTheme;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
112
|
export function ThemeProvider({
|
|
134
113
|
config,
|
|
135
114
|
initialTheme,
|
|
@@ -137,17 +116,48 @@ export function ThemeProvider({
|
|
|
137
116
|
}: ThemeProviderProps): React.ReactNode {
|
|
138
117
|
const [mounted, setMounted] = useState(false);
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
119
|
+
// HYDRATION PARITY: this initializer is the server (SSR/resume) render AND
|
|
120
|
+
// the client's hydration render — both must produce the same value. It must
|
|
121
|
+
// NEVER read cookie/localStorage: whenever the payload's initialTheme
|
|
122
|
+
// differs from the visitor's stored theme (a PPR shell HIT deliberately
|
|
123
|
+
// replays the CAPTURE's initialTheme), a storage-reading initializer makes
|
|
124
|
+
// the client's first render diverge from the server tree. Any raw-theme text
|
|
125
|
+
// (a toggle label) then fails hydration, React regenerates the tree, and the
|
|
126
|
+
// FOUC-applied class is wiped from <html>. The visitor's stored theme is
|
|
127
|
+
// applied by the post-mount re-sync effect below instead.
|
|
128
|
+
const [theme, setThemeState] = useState<Theme>(
|
|
129
|
+
() => initialTheme ?? config.defaultTheme,
|
|
130
|
+
);
|
|
145
131
|
|
|
146
132
|
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
|
|
147
133
|
|
|
148
134
|
useEffect(() => {
|
|
149
135
|
setMounted(true);
|
|
150
136
|
setSystemTheme(getSystemTheme());
|
|
137
|
+
// Re-sync state from an EXPLICITLY stored theme after mount. initialTheme
|
|
138
|
+
// comes from the payload and can legitimately differ from the visitor's
|
|
139
|
+
// stored theme — on a PPR shell HIT it is deliberately the CAPTURE's theme
|
|
140
|
+
// (the resume tree must match the frozen prelude; see
|
|
141
|
+
// ShellCacheEntry.initialTheme). The FOUC script already applied the stored
|
|
142
|
+
// theme to the document pre-paint; this brings the provider state (toggles,
|
|
143
|
+
// useTheme readers) in line with it, and is the ONLY place the provider
|
|
144
|
+
// reads storage (the initializer must not — see the parity note above).
|
|
145
|
+
// Only an explicit cookie/localStorage value re-syncs — a defaultTheme
|
|
146
|
+
// fallback must not override a server-provided initialTheme when the
|
|
147
|
+
// visitor never chose a theme.
|
|
148
|
+
// First VALID candidate wins — an empty/garbage cookie value (e.g. a
|
|
149
|
+
// deleted cookie leaving "theme=") must not shadow a valid localStorage
|
|
150
|
+
// value behind a bare null-coalesce.
|
|
151
|
+
const explicit =
|
|
152
|
+
[
|
|
153
|
+
readThemeFromCookie(config.storageKey),
|
|
154
|
+
readThemeFromStorage(config.storageKey),
|
|
155
|
+
].find((v) => v !== null && isValidTheme(v, config)) ?? null;
|
|
156
|
+
if (explicit !== null && explicit !== theme) {
|
|
157
|
+
setThemeState(explicit as Theme);
|
|
158
|
+
applyThemeToDocument(explicit as Theme, config);
|
|
159
|
+
}
|
|
160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
161
|
}, []);
|
|
152
162
|
|
|
153
163
|
const setTheme = useCallback(
|
|
@@ -504,7 +504,7 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
504
504
|
* @param args.context - App context (db, user, etc.)
|
|
505
505
|
* @param args.actionResult - Result from action (future support)
|
|
506
506
|
* @param args.formData - Form data from action (future support)
|
|
507
|
-
* @param args.
|
|
507
|
+
* @param args.method - HTTP method: "GET" for navigation, "POST" for server actions
|
|
508
508
|
*
|
|
509
509
|
* @returns Hard decision (boolean), soft suggestion (object), or defer
|
|
510
510
|
* (`void` / `null` / `undefined`) to keep the running suggestion as-is.
|
package/src/types/index.ts
CHANGED
|
@@ -10,20 +10,29 @@ export type DocumentProps = {
|
|
|
10
10
|
type ParseConstraint<T extends string> =
|
|
11
11
|
T extends `${infer First}|${infer Rest}` ? First | ParseConstraint<Rest> : T;
|
|
12
12
|
|
|
13
|
+
// Named catch-all (`:name*` / `:name+`) is matched BEFORE the `?`/suffix
|
|
14
|
+
// branches. Its modifier is anchored to the END of the token (no trailing
|
|
15
|
+
// `${string}`) so it is a true suffix and never mis-splits a constraint body
|
|
16
|
+
// such as `id(\d+)`. Both are a required `string`: a matched catch-all always
|
|
17
|
+
// binds a value (possibly ""), so the key is always present.
|
|
13
18
|
type ExtractParamInfo<T extends string> =
|
|
14
19
|
T extends `${infer Name}(${infer Constraint})?${string}`
|
|
15
20
|
? { name: Name; optional: true; type: ParseConstraint<Constraint> }
|
|
16
21
|
: T extends `${infer Name}(${infer Constraint})${string}`
|
|
17
22
|
? { name: Name; optional: false; type: ParseConstraint<Constraint> }
|
|
18
|
-
: T extends `${infer Name}
|
|
19
|
-
? { name: Name; optional:
|
|
20
|
-
: T extends `${infer Name}
|
|
23
|
+
: T extends `${infer Name}*`
|
|
24
|
+
? { name: Name; optional: false; type: string }
|
|
25
|
+
: T extends `${infer Name}+`
|
|
21
26
|
? { name: Name; optional: false; type: string }
|
|
22
|
-
: T extends `${infer Name}
|
|
23
|
-
? { name: Name; optional:
|
|
24
|
-
: T extends `${infer Name}
|
|
27
|
+
: T extends `${infer Name}?${string}`
|
|
28
|
+
? { name: Name; optional: true; type: string }
|
|
29
|
+
: T extends `${infer Name}.${string}`
|
|
25
30
|
? { name: Name; optional: false; type: string }
|
|
26
|
-
:
|
|
31
|
+
: T extends `${infer Name}-${string}`
|
|
32
|
+
? { name: Name; optional: false; type: string }
|
|
33
|
+
: T extends `${infer Name}~${string}`
|
|
34
|
+
? { name: Name; optional: false; type: string }
|
|
35
|
+
: { name: T; optional: false; type: string };
|
|
27
36
|
|
|
28
37
|
type ParamFromInfo<Info> = Info extends {
|
|
29
38
|
name: infer N extends string;
|
|
@@ -51,12 +60,15 @@ type MergeParams<A, B> = Pick<A, keyof A> & Pick<B, keyof B> extends infer O
|
|
|
51
60
|
* - Optional params: /:locale? -> { locale?: string }
|
|
52
61
|
* - Constrained params: /:locale(en|gb) -> { locale: "en" | "gb" }
|
|
53
62
|
* - Optional + constrained: /:locale(en|gb)? -> { locale?: "en" | "gb" }
|
|
63
|
+
* - Named catch-all: /:path+ (one-or-more), /:slug* (zero-or-more) -> string
|
|
54
64
|
*
|
|
55
65
|
* @example
|
|
56
66
|
* ExtractParams<"/products/:id"> // { id: string }
|
|
57
67
|
* ExtractParams<"/:locale?/blog/:slug"> // { locale?: string; slug: string }
|
|
58
68
|
* ExtractParams<"/:locale(en|gb)/blog"> // { locale: "en" | "gb" }
|
|
59
69
|
* ExtractParams<"/:locale(en|gb)?/blog/:slug"> // { locale?: "en" | "gb"; slug: string }
|
|
70
|
+
* ExtractParams<"/docs/:slug*"> // { slug: string }
|
|
71
|
+
* ExtractParams<"/shop/:path+"> // { path: string }
|
|
60
72
|
*/
|
|
61
73
|
export type ExtractParams<
|
|
62
74
|
T extends string,
|
package/src/types/segments.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { ErrorInfo, NotFoundInfo } from "./boundaries.js";
|
|
3
|
+
import type { RevalidateParams, HandlerContext } from "./handler-context.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* CSS class(es) for a ViewTransition phase.
|
|
@@ -8,6 +9,96 @@ import type { ErrorInfo, NotFoundInfo } from "./boundaries.js";
|
|
|
8
9
|
*/
|
|
9
10
|
export type ViewTransitionClass = Record<string, string> | string;
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The context a transition({ when }) predicate receives.
|
|
14
|
+
*
|
|
15
|
+
* It mirrors the {@link ShouldRevalidateFn} args a `revalidate()` predicate
|
|
16
|
+
* gets — the same navigation/action metadata — so the two read the same shape,
|
|
17
|
+
* plus `get`/`env` for post-handler reads. There is no full `HandlerContext`
|
|
18
|
+
* here: the gate runs at the RSC-payload layer with the request context, not a
|
|
19
|
+
* handler context, so handler-only sugar (`search`/`build`/`dev`/`headers`) is
|
|
20
|
+
* absent by design. `get` is the way to read what the handler/middleware set
|
|
21
|
+
* via `ctx.set(...)` this request.
|
|
22
|
+
*
|
|
23
|
+
* Field availability (all source fields are optional — never fabricated):
|
|
24
|
+
* - `currentUrl` / `currentParams` / `fromRouteName` (the navigation SOURCE) are
|
|
25
|
+
* populated on soft navigations and action-success revalidations. They are
|
|
26
|
+
* undefined on an initial full document load and on action-error / no-JS error
|
|
27
|
+
* paths that skip the navigation snapshot — there is no prior page to name.
|
|
28
|
+
* - `nextUrl` / `nextParams` / `get` / `env` / `method` are always present;
|
|
29
|
+
* `toRouteName` is present only when the target route is named (undefined for
|
|
30
|
+
* unnamed/auto-generated routes, like `fromRouteName`).
|
|
31
|
+
* - `actionId` / `actionUrl` / `actionResult` / `formData` are populated only
|
|
32
|
+
* when a server action triggered the render; `method` is "POST" then, "GET"
|
|
33
|
+
* otherwise. On no-JS (progressive-enhancement) action paths `actionId` may be
|
|
34
|
+
* undefined when React cannot surface the action's stable id: the success
|
|
35
|
+
* re-render still sets `actionUrl`/`formData` for a recognized action, but the
|
|
36
|
+
* error-boundary re-render exposes `actionUrl` only when `actionId` resolved.
|
|
37
|
+
* Malformed form bodies that fail before action detection expose no action
|
|
38
|
+
* fields. Treat `actionId` as "the action, if known", not as "was this an
|
|
39
|
+
* action".
|
|
40
|
+
*
|
|
41
|
+
* PREFETCH / CACHE CAVEAT (read this before gating on the source): the gate runs
|
|
42
|
+
* server-side during resolution. A PREFETCHED navigation renders at prefetch
|
|
43
|
+
* time, so `currentUrl`/`currentParams`/`fromRouteName` reflect the page the
|
|
44
|
+
* prefetch fired from, NOT necessarily the page the user actually navigates from
|
|
45
|
+
* — the decision is baked into the stored Flight payload and replayed verbatim.
|
|
46
|
+
* A `cache()`/prerender hit replays the stored transition with the predicate NOT
|
|
47
|
+
* re-run at all. So a source-sensitive predicate can be frozen to prefetch-time
|
|
48
|
+
* or store-time state. This is accepted (~99% of navigations match), but if your
|
|
49
|
+
* gate must reflect the exact click-time source, source-scope the prefetch
|
|
50
|
+
* (`<Link prefetchKey=":source">`) and do not `cache()` that segment.
|
|
51
|
+
*/
|
|
52
|
+
export type TransitionWhenContext<
|
|
53
|
+
TParams = Record<string, string>,
|
|
54
|
+
TEnv = unknown,
|
|
55
|
+
> = Partial<
|
|
56
|
+
Pick<
|
|
57
|
+
RevalidateParams<TParams, TEnv>,
|
|
58
|
+
"currentUrl" | "currentParams" | "fromRouteName"
|
|
59
|
+
>
|
|
60
|
+
> &
|
|
61
|
+
Pick<
|
|
62
|
+
RevalidateParams<TParams, TEnv>,
|
|
63
|
+
| "nextUrl"
|
|
64
|
+
| "nextParams"
|
|
65
|
+
| "toRouteName"
|
|
66
|
+
| "actionId"
|
|
67
|
+
| "actionUrl"
|
|
68
|
+
| "actionResult"
|
|
69
|
+
| "formData"
|
|
70
|
+
| "method"
|
|
71
|
+
> &
|
|
72
|
+
Pick<HandlerContext<any, TEnv>, "get" | "env">;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Predicate that gates whether a transition() applies for the current request.
|
|
76
|
+
*
|
|
77
|
+
* Evaluated server-side AFTER the route's handler runs (so `get(...)` can read
|
|
78
|
+
* handler/middleware-set state) and outside any cache scope. Return false to
|
|
79
|
+
* drop this segment's transition for the request; return true to apply it. The
|
|
80
|
+
* context ({@link TransitionWhenContext}) carries the same navigation/action
|
|
81
|
+
* metadata a `revalidate()` predicate sees plus `get`/`env`. If it throws, the
|
|
82
|
+
* error is reported to the router's onError (phase "rendering") and the
|
|
83
|
+
* transition is dropped (the navigation does not hold).
|
|
84
|
+
*
|
|
85
|
+
* Distinct from intercept()'s `when` config selector, which runs at MATCH time
|
|
86
|
+
* over `{ from, to, params, segments, … }`; a transition `when` runs
|
|
87
|
+
* post-handler over the resolved payload.
|
|
88
|
+
*
|
|
89
|
+
* Scope: dropping a transition removes only THIS segment's contribution to the
|
|
90
|
+
* navigation's hold. The startTransition hold is navigation-wide — it engages if
|
|
91
|
+
* any matched segment still has a transition — so `when: false` makes the
|
|
92
|
+
* navigation stream its loading fallback only when no other matched segment
|
|
93
|
+
* keeps a transition (the common case: a single transition on the route).
|
|
94
|
+
*
|
|
95
|
+
* Evaluated on every fresh (cache-miss) resolution; it is NOT re-run when a
|
|
96
|
+
* segment is replayed from the runtime cache or a build-time prerender, and a
|
|
97
|
+
* prefetched navigation freezes it to prefetch-time state — see the caveat on
|
|
98
|
+
* {@link TransitionWhenContext}.
|
|
99
|
+
*/
|
|
100
|
+
export type TransitionWhenFn = (ctx: TransitionWhenContext) => boolean;
|
|
101
|
+
|
|
11
102
|
/**
|
|
12
103
|
* Configuration for React's <ViewTransition> component.
|
|
13
104
|
*
|
|
@@ -36,6 +127,15 @@ export interface TransitionConfig {
|
|
|
36
127
|
* When unset, inherits the createRouter({ viewTransition }) default.
|
|
37
128
|
*/
|
|
38
129
|
viewTransition?: "auto" | false;
|
|
130
|
+
/**
|
|
131
|
+
* Optional server-side predicate that gates this transition per request. When
|
|
132
|
+
* present and it returns false (evaluated post-handler), the router drops this
|
|
133
|
+
* segment's transition for the request, so the navigation streams its loading
|
|
134
|
+
* fallback instead of holding. The predicate is server-only and never
|
|
135
|
+
* serialized to the client; only its resolved effect (transition kept or
|
|
136
|
+
* dropped) crosses. See {@link TransitionWhenFn}.
|
|
137
|
+
*/
|
|
138
|
+
when?: TransitionWhenFn;
|
|
39
139
|
}
|
|
40
140
|
|
|
41
141
|
/**
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validateUserRouteName,
|
|
10
10
|
} from "../route-name.js";
|
|
11
11
|
import type { UrlPatterns, IncludeOptions } from "./pattern-types.js";
|
|
12
|
+
import type { IncludeProvider } from "./include-provider.js";
|
|
12
13
|
import type { IncludeFn } from "./path-helper-types.js";
|
|
13
14
|
|
|
14
15
|
function hasExplicitNameOption(options: IncludeOptions | undefined): boolean {
|
|
@@ -60,7 +61,11 @@ export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
|
|
|
60
61
|
export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
61
62
|
return (
|
|
62
63
|
prefix: string,
|
|
63
|
-
|
|
64
|
+
// A `urls()` value (eager) OR an async provider thunk
|
|
65
|
+
// (`() => import("./routes")`) whose evaluation is deferred to the first
|
|
66
|
+
// request matching `prefix`. The provider is stored unevaluated and
|
|
67
|
+
// resolved by the runtime lazy-include expansion / build-time discovery.
|
|
68
|
+
patterns: UrlPatterns<TEnv> | IncludeProvider<TEnv>,
|
|
64
69
|
options?: IncludeOptions,
|
|
65
70
|
): IncludeItem => {
|
|
66
71
|
const { ctx } = requireDslContext("include() must be called inside urls()");
|
|
@@ -81,10 +86,9 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
81
86
|
? capturedUrlPrefix + prefix.slice(1)
|
|
82
87
|
: capturedUrlPrefix + prefix
|
|
83
88
|
: prefix;
|
|
84
|
-
const
|
|
85
|
-
?
|
|
86
|
-
:
|
|
87
|
-
const nextSegment = hasExplicitName ? explicitName : internalScope;
|
|
89
|
+
const nextSegment = hasExplicitName
|
|
90
|
+
? explicitName
|
|
91
|
+
: allocateInternalIncludeScopeId(ctx.counters);
|
|
88
92
|
const fullNamePrefix =
|
|
89
93
|
nextSegment !== undefined && nextSegment !== ""
|
|
90
94
|
? capturedNamePrefix
|
|
@@ -117,9 +121,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
117
121
|
if (capturedParent?.shortCode) {
|
|
118
122
|
const includeCounterKey = `${capturedParent.shortCode}${parentScope}_include`;
|
|
119
123
|
ctx.counters[includeCounterKey] ??= 0;
|
|
120
|
-
|
|
121
|
-
ctx.counters[includeCounterKey] = includeIdx + 1;
|
|
122
|
-
includeScope = `${parentScope}I${includeIdx}`;
|
|
124
|
+
includeScope = `${parentScope}I${ctx.counters[includeCounterKey]++}`;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
// Snapshot parent's counters AFTER allocating the include scope so lazy
|