@rangojs/router 0.0.0-experimental.136 → 0.0.0-experimental.137
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +29 -12
- package/skills/testing/handles.md +4 -2
- package/src/browser/event-controller.ts +21 -0
- package/src/browser/react/NavigationProvider.tsx +68 -157
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -88
- 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/index.rsc.ts +0 -1
- package/src/index.ts +0 -1
- package/src/prerender.ts +17 -4
- package/src/router/prerender-match.ts +7 -3
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/progressive-enhancement.ts +5 -2
- package/src/rsc/rsc-rendering.ts +5 -1
- package/src/ssr/index.tsx +1 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/render-route.tsx +6 -1
- package/src/browser/react/deferred-handle-resolution.ts +0 -75
|
@@ -15,6 +15,7 @@ import type { MiddlewareFn } from "../router/middleware.js";
|
|
|
15
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
16
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
17
|
import { gateTransitions } from "./transition-gate.js";
|
|
18
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
18
19
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
19
20
|
import {
|
|
20
21
|
createResponseWithMergedHeaders,
|
|
@@ -315,7 +316,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
315
316
|
params: match.params,
|
|
316
317
|
isPartial: false,
|
|
317
318
|
rootLayout: ctx.router.rootLayout,
|
|
318
|
-
|
|
319
|
+
// PE full render: resolve deferred handle values server-side.
|
|
320
|
+
handles: resolvedHandleStream(handleStore),
|
|
319
321
|
version: ctx.version,
|
|
320
322
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
321
323
|
themeConfig: ctx.router.themeConfig,
|
|
@@ -462,7 +464,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
462
464
|
isPartial: false,
|
|
463
465
|
isError: true,
|
|
464
466
|
rootLayout: ctx.router.rootLayout,
|
|
465
|
-
|
|
467
|
+
// PE error-boundary full render: resolve deferred handle values server-side.
|
|
468
|
+
handles: resolvedHandleStream(handleStore),
|
|
466
469
|
version: ctx.version,
|
|
467
470
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
468
471
|
themeConfig: ctx.router.themeConfig,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./helpers.js";
|
|
23
23
|
import type { HandlerContext } from "./handler-context.js";
|
|
24
24
|
import { gateTransitions } from "./transition-gate.js";
|
|
25
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
25
26
|
|
|
26
27
|
export function handleRscRendering<TEnv>(
|
|
27
28
|
ctx: HandlerContext<TEnv>,
|
|
@@ -78,7 +79,10 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
78
79
|
params: m.params,
|
|
79
80
|
isPartial: false,
|
|
80
81
|
rootLayout: ctx.router.rootLayout,
|
|
81
|
-
|
|
82
|
+
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
83
|
+
// the first sync useHandle read see resolved values. Partial payloads below
|
|
84
|
+
// keep streaming (handleStore.stream()).
|
|
85
|
+
handles: resolvedHandleStream(handleStore),
|
|
82
86
|
version: ctx.version,
|
|
83
87
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
84
88
|
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
package/src/ssr/index.tsx
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* It relies on createHandle registering the collect even in a bare test (it
|
|
13
13
|
* assigns a runtime fallback id when the Vite plugin did not inject one). If a
|
|
14
14
|
* handle's module was never imported (so createHandle never ran), the collect is
|
|
15
|
-
* unregistered and this falls back to
|
|
15
|
+
* unregistered and this falls back to the default identity collect — with a
|
|
16
|
+
* warning, since a CUSTOM collect that failed to register silently returns the
|
|
17
|
+
* wrong shape.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { getCollectFn, type Handle } from "../handle.js";
|
|
@@ -25,16 +27,20 @@ export function collectHandle<TData, TAccumulated>(
|
|
|
25
27
|
| ((segments: TData[][]) => TAccumulated)
|
|
26
28
|
| undefined;
|
|
27
29
|
|
|
30
|
+
// Drop empty arrays matching production behavior (segment count/indices).
|
|
31
|
+
const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
|
|
32
|
+
|
|
33
|
+
// No registered collect (the handle's module was not imported): fall back to the
|
|
34
|
+
// default identity collect — the per-segment arrays as-is, mirroring production
|
|
35
|
+
// collectHandleData. Warn, because a handle with a CUSTOM collect would silently
|
|
36
|
+
// get the wrong shape (the runtime can't tell it from an intended default).
|
|
28
37
|
if (!collectFn) {
|
|
29
38
|
console.warn(
|
|
30
|
-
`[rango] collectHandle: handle "${handle.$$id}" has no registered collect ` +
|
|
31
|
-
`
|
|
32
|
-
`
|
|
39
|
+
`[rango] collectHandle: handle "${handle.$$id}" has no registered collect — ` +
|
|
40
|
+
`falling back to the identity (per-segment data as-is). Import the handle's ` +
|
|
41
|
+
`module so createHandle() runs if you expected a custom collect.`,
|
|
33
42
|
);
|
|
34
|
-
return
|
|
43
|
+
return nonEmpty as unknown as TAccumulated;
|
|
35
44
|
}
|
|
36
|
-
|
|
37
|
-
// Drop empty arrays matching production behavior (segment count/indices).
|
|
38
|
-
const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
|
|
39
45
|
return collectFn(nonEmpty);
|
|
40
46
|
}
|
|
@@ -46,6 +46,7 @@ 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";
|
|
@@ -481,8 +482,12 @@ export async function renderRoute(
|
|
|
481
482
|
|
|
482
483
|
const eventController = createEventController({ initialLocation: url });
|
|
483
484
|
eventController.setParams(initialMatch.params);
|
|
485
|
+
// Resolve-by-default: resolve any deferred (Promise) seeded handle values
|
|
486
|
+
// before applying, so the seeded handles reach collect/useHandle resolved —
|
|
487
|
+
// matching what the server/client do in a real app.
|
|
488
|
+
const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
|
|
484
489
|
eventController.setHandleData(
|
|
485
|
-
|
|
490
|
+
resolvedSeed,
|
|
486
491
|
initialSegments.map((s) => s.id),
|
|
487
492
|
);
|
|
488
493
|
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { HandleData } from "../types.js";
|
|
2
|
-
import { isThenable } from "../../handles/is-thenable.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The set of handle names whose deferred (Promise) values MUST be resolved in
|
|
6
|
-
* the store BEFORE the snapshot is applied during client navigation.
|
|
7
|
-
*
|
|
8
|
-
* The boundary: a handle belongs here only if its consumer `use()`s a promise in
|
|
9
|
-
* <head>, above the route's <Suspense>. Suspending there would revert the
|
|
10
|
-
* just-committed route and hide its loading fallback. Today that is Meta alone
|
|
11
|
-
* (MetaTags lives in <head> and use()s deferred descriptors). Every OTHER handle
|
|
12
|
-
* keeps the public DeferredHandleEntry contract: its deferred value reaches the
|
|
13
|
-
* consumer AS A PROMISE during soft navigation, narrowed via isThenable().
|
|
14
|
-
*
|
|
15
|
-
* If a future head-placed handle starts use()-ing promises, add its name here.
|
|
16
|
-
*/
|
|
17
|
-
export const HEAD_RESOLVE_HANDLE_NAMES: readonly string[] = [
|
|
18
|
-
"__rsc_router_meta__",
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* True when a handle value in this snapshot is a deferred (Promise) value.
|
|
23
|
-
*
|
|
24
|
-
* When `onlyHandleNames` is given, only those handle buckets are considered;
|
|
25
|
-
* deferred values under any other handle are ignored (they pass through to the
|
|
26
|
-
* consumer as promises, by contract).
|
|
27
|
-
*/
|
|
28
|
-
export function hasDeferredHandleValue(
|
|
29
|
-
data: HandleData,
|
|
30
|
-
onlyHandleNames?: readonly string[],
|
|
31
|
-
): boolean {
|
|
32
|
-
const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
|
|
33
|
-
for (const [handleName, segments] of Object.entries(data)) {
|
|
34
|
-
if (scope && !scope.has(handleName)) continue;
|
|
35
|
-
for (const values of Object.values(segments)) {
|
|
36
|
-
if (values.some(isThenable)) return true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Snapshot with deferred (Promise) values awaited; a rejected deferred is
|
|
44
|
-
* dropped (it contributes nothing), mirroring the render-side REJECTED_META.
|
|
45
|
-
* Promise.allSettled treats non-promise values as already-fulfilled, so plain
|
|
46
|
-
* values pass through unchanged.
|
|
47
|
-
*
|
|
48
|
-
* When `onlyHandleNames` is given, ONLY those handle buckets are resolved; every
|
|
49
|
-
* other bucket is copied through by reference (its deferred values keep their
|
|
50
|
-
* promise identity so the consumer can narrow them).
|
|
51
|
-
*/
|
|
52
|
-
export async function resolveDeferredHandleValues(
|
|
53
|
-
data: HandleData,
|
|
54
|
-
onlyHandleNames?: readonly string[],
|
|
55
|
-
): Promise<HandleData> {
|
|
56
|
-
const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
|
|
57
|
-
const out: HandleData = {};
|
|
58
|
-
await Promise.all(
|
|
59
|
-
Object.entries(data).flatMap(([handleName, segments]) => {
|
|
60
|
-
// Out-of-scope buckets pass through untouched (promise identity kept).
|
|
61
|
-
if (scope && !scope.has(handleName)) {
|
|
62
|
-
out[handleName] = segments;
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
out[handleName] = {};
|
|
66
|
-
return Object.entries(segments).map(async ([segmentId, values]) => {
|
|
67
|
-
const settled = await Promise.allSettled(values);
|
|
68
|
-
out[handleName][segmentId] = settled
|
|
69
|
-
.filter((r) => r.status === "fulfilled")
|
|
70
|
-
.map((r) => (r as PromiseFulfilledResult<unknown>).value);
|
|
71
|
-
});
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
return out;
|
|
75
|
-
}
|