@timber-js/app 0.1.53 → 0.1.55
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/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +8 -4
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/client/index.js +59 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +6 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +8 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts +25 -2
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +1 -1
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nitro.ts +8 -4
- package/src/client/browser-entry.ts +6 -6
- package/src/client/router.ts +24 -21
- package/src/client/segment-cache.ts +14 -5
- package/src/client/segment-context.ts +8 -1
- package/src/client/segment-merger.ts +55 -7
- package/src/server/route-element-builder.ts +42 -6
- package/src/server/state-tree-diff.ts +18 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/client/router.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/client/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,10 +46,15 @@ export declare class SegmentCache {
|
|
|
46
46
|
* Only includes sync segments — async segments are excluded because the
|
|
47
47
|
* server must always re-render them (they may depend on request context).
|
|
48
48
|
*
|
|
49
|
+
* When mergeableFilter is provided, only segments whose paths are in the
|
|
50
|
+
* set are included. This ensures the server only skips segments that the
|
|
51
|
+
* client can actually merge (i.e., segments whose cached element tree
|
|
52
|
+
* contains an inner SegmentProvider the merger can splice into).
|
|
53
|
+
*
|
|
49
54
|
* This is a performance optimization only, NOT a security boundary.
|
|
50
55
|
* The server always runs all access.ts files regardless of the state tree.
|
|
51
56
|
*/
|
|
52
|
-
serializeStateTree(): StateTree;
|
|
57
|
+
serializeStateTree(mergeableFilter?: Set<string>): StateTree;
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
55
60
|
* Segment metadata from the server, sent via X-Timber-Segments header.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment-cache.d.ts","sourceRoot":"","sources":["../../src/client/segment-cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAI1C,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAClD,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAA0B;IAEtC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAO7C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI;IAM7C,KAAK,IAAI,IAAI;IAIb
|
|
1
|
+
{"version":3,"file":"segment-cache.d.ts","sourceRoot":"","sources":["../../src/client/segment-cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAI1C,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,uFAAuF;IACvF,WAAW,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAClD,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAID;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAA0B;IAEtC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAO7C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI;IAM7C,KAAK,IAAI,IAAI;IAIb;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS;CAO7D;AAkBD;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,WAAW,GAAG,SAAS,CA+BjF;AASD;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAU;IACxC,OAAO,CAAC,OAAO,CAAoC;IAEnD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAO9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAU5C,0EAA0E;IAC1E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;CAOjD"}
|
|
@@ -20,6 +20,13 @@ export interface SegmentContextValue {
|
|
|
20
20
|
export declare function useSegmentContext(): SegmentContextValue | null;
|
|
21
21
|
interface SegmentProviderProps {
|
|
22
22
|
segments: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Unique identifier for this segment, used by the client-side segment
|
|
25
|
+
* merger for element caching. For route groups this includes the group
|
|
26
|
+
* name (e.g., "/(marketing)") since groups share their parent's urlPath.
|
|
27
|
+
* Falls back to the reconstructed path from `segments` if not provided.
|
|
28
|
+
*/
|
|
29
|
+
segmentId?: string;
|
|
23
30
|
parallelRouteKeys: string[];
|
|
24
31
|
children: React.ReactNode;
|
|
25
32
|
}
|
|
@@ -27,6 +34,6 @@ interface SegmentProviderProps {
|
|
|
27
34
|
* Wraps each layout to provide segment position context.
|
|
28
35
|
* Injected by rsc-entry.ts during element tree construction.
|
|
29
36
|
*/
|
|
30
|
-
export declare function SegmentProvider({ segments, parallelRouteKeys, children }: SegmentProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SegmentContextValue | null>>;
|
|
37
|
+
export declare function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, children }: SegmentProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SegmentContextValue | null>>;
|
|
31
38
|
export {};
|
|
32
39
|
//# sourceMappingURL=segment-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment-context.d.ts","sourceRoot":"","sources":["../../src/client/segment-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0FAA0F;IAC1F,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAMD,qFAAqF;AACrF,wBAAgB,iBAAiB,IAAI,mBAAmB,GAAG,IAAI,CAE9D;AAID,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"segment-context.d.ts","sourceRoot":"","sources":["../../src/client/segment-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0FAA0F;IAC1F,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAMD,qFAAqF;AACrF,wBAAgB,iBAAiB,IAAI,mBAAmB,GAAG,IAAI,CAE9D;AAID,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,oBAAoB,uGASrH"}
|
|
@@ -30,6 +30,13 @@ export interface CachedSegmentEntry {
|
|
|
30
30
|
segmentPath: string;
|
|
31
31
|
/** The SegmentProvider element for this segment */
|
|
32
32
|
element: ReactElement;
|
|
33
|
+
/**
|
|
34
|
+
* Whether this segment's cached element contains a nested SegmentProvider.
|
|
35
|
+
* Only segments with inner SegmentProviders are safe to skip — the merger
|
|
36
|
+
* can only replace inner SegmentProviders, not pages embedded in layout output.
|
|
37
|
+
* Used by the state tree serialization to exclude non-mergeable segments.
|
|
38
|
+
*/
|
|
39
|
+
hasMergeableChild: boolean;
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Cache of React element subtrees per segment path.
|
|
@@ -42,6 +49,20 @@ export declare class SegmentElementCache {
|
|
|
42
49
|
has(segmentPath: string): boolean;
|
|
43
50
|
clear(): void;
|
|
44
51
|
get size(): number;
|
|
52
|
+
/**
|
|
53
|
+
* Get the set of segment paths that are safe for the server to skip.
|
|
54
|
+
* Only segments with an inner SegmentProvider (hasMergeableChild) are
|
|
55
|
+
* included — the merger can only replace inner SegmentProviders, not
|
|
56
|
+
* pages embedded in layout output. Used to filter the state tree.
|
|
57
|
+
*
|
|
58
|
+
* Returns an empty set if the element cache is empty (no elements
|
|
59
|
+
* cached yet). This is the safe default — an empty set means no
|
|
60
|
+
* segments pass the filter, so the state tree is empty and the server
|
|
61
|
+
* does a full render. The element cache is populated lazily after the
|
|
62
|
+
* first SPA navigation (RSC-decoded elements from hydration are
|
|
63
|
+
* thenables that can't be walked until React resolves them).
|
|
64
|
+
*/
|
|
65
|
+
getMergeablePaths(): Set<string>;
|
|
45
66
|
}
|
|
46
67
|
/**
|
|
47
68
|
* Check if a React element is a SegmentProvider by looking for the
|
|
@@ -51,8 +72,10 @@ export declare class SegmentElementCache {
|
|
|
51
72
|
export declare function isSegmentProvider(element: unknown): element is ReactElement;
|
|
52
73
|
/**
|
|
53
74
|
* Extract the segment path from a SegmentProvider element.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* Uses the `segmentId` prop if available (set by the server for route groups
|
|
77
|
+
* to distinguish siblings that share the same urlPath). Falls back to
|
|
78
|
+
* reconstructing from the `segments` array prop.
|
|
56
79
|
*/
|
|
57
80
|
export declare function getSegmentPath(element: ReactElement): string;
|
|
58
81
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment-merger.d.ts","sourceRoot":"","sources":["../../src/client/segment-merger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAgC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIxF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,OAAO,EAAE,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"segment-merger.d.ts","sourceRoot":"","sources":["../../src/client/segment-merger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAgC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIxF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,OAAO,EAAE,YAAY,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAID;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAyC;IAExD,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIxD,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAIzD,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIjC,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;;;;;;OAYG;IACH,iBAAiB,IAAI,GAAG,CAAC,MAAM,CAAC;CASjC;AAID;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,YAAY,CAI3E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAM5D;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,kBAAkB,EAAE,CAUtE;AAuCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,mBAAmB,GACzB,IAAI,CAKN;AAqDD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,YAAY,EAC3B,eAAe,EAAE,SAAS,EAC1B,gBAAgB,CAAC,EAAE,MAAM,GACxB,YAAY,CAoCd;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,MAAM,EAAE,EACzB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CA0BT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAO7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAKzD,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,2CAA2C;IAC3C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,MAAM,EAAE,UAAU,GAAG,cAAc;aACnC,gBAAgB,EAAE,oBAAoB,EAAE;aACxC,QAAQ,EAAE,mBAAmB,EAAE;gBAF/B,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,QAAQ,EAAE,mBAAmB,EAAE;CAIlD;AAID;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GACnC,OAAO,CAAC,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAO7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAKzD,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,2CAA2C;IAC3C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,MAAM,EAAE,UAAU,GAAG,cAAc;aACnC,gBAAgB,EAAE,oBAAoB,EAAE;aACxC,QAAQ,EAAE,mBAAmB,EAAE;gBAF/B,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,QAAQ,EAAE,mBAAmB,EAAE;CAIlD;AAID;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GACnC,OAAO,CAAC,kBAAkB,CAAC,CA2W7B"}
|
|
@@ -39,5 +39,5 @@ export declare function parseClientStateTree(req: Request): Set<string> | null;
|
|
|
39
39
|
* @param isLeaf - Whether this is the leaf segment (page segment)
|
|
40
40
|
* @param clientSegments - Set of paths from X-Timber-State-Tree, or null
|
|
41
41
|
*/
|
|
42
|
-
export declare function shouldSkipSegment(
|
|
42
|
+
export declare function shouldSkipSegment(_urlPath: string, _layoutComponent: ((...args: unknown[]) => unknown) | undefined, _isLeaf: boolean, _clientSegments: Set<string> | null): boolean;
|
|
43
43
|
//# sourceMappingURL=state-tree-diff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAarE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,
|
|
1
|
+
{"version":3,"file":"state-tree-diff.d.ts","sourceRoot":"","sources":["../../src/server/state-tree-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAarE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,EAC/D,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAClC,OAAO,CAeT"}
|
package/package.json
CHANGED
package/src/adapters/nitro.ts
CHANGED
|
@@ -341,20 +341,24 @@ export function generateNitroEntry(
|
|
|
341
341
|
return `// Generated by @timber-js/app/adapters/nitro
|
|
342
342
|
// Do not edit — this file is regenerated on each build.
|
|
343
343
|
|
|
344
|
-
${manifestImport}import {
|
|
345
|
-
import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'
|
|
344
|
+
${manifestImport}import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'
|
|
346
345
|
import { compressResponse } from './_compress.mjs'
|
|
347
346
|
|
|
348
347
|
// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
|
|
349
348
|
// See design/25-production-deployments.md §"TIMBER_RUNTIME".
|
|
350
349
|
process.env.TIMBER_RUNTIME = '${runtimeName}'
|
|
351
350
|
|
|
352
|
-
|
|
351
|
+
// Nitro's index.mjs wraps this import with defineLazyEventHandler, which
|
|
352
|
+
// already handles the event handler protocol. We export a plain async
|
|
353
|
+
// function — no defineEventHandler wrapper needed.
|
|
354
|
+
// Nitro bundles h3 into its internal _libs/ directory but doesn't make
|
|
355
|
+
// it available as a bare specifier in the output's node_modules.
|
|
356
|
+
export default async function timberHandler(event) {
|
|
353
357
|
// h3 v2: event.req is the Web Request
|
|
354
358
|
const webRequest = event.req
|
|
355
359
|
${handlerCall}
|
|
356
360
|
return compressResponse(webRequest, webResponse)
|
|
357
|
-
}
|
|
361
|
+
}
|
|
358
362
|
`;
|
|
359
363
|
}
|
|
360
364
|
|
|
@@ -545,12 +545,12 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
545
545
|
delete (self as unknown as Record<string, unknown>).__timber_segments;
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
548
|
+
// NOTE: We do NOT cache segment elements from the initial RSC payload here.
|
|
549
|
+
// The decoded element from createFromReadableStream is a thenable/lazy
|
|
550
|
+
// element that React resolves during render — the segment walker can't
|
|
551
|
+
// traverse it. The element cache is populated lazily after the first SPA
|
|
552
|
+
// navigation (via mergeAndCachePayload in renderViaTransition), when
|
|
553
|
+
// the decoded payload is a fully resolved React element tree.
|
|
554
554
|
|
|
555
555
|
// Note: __timber_params is read before hydrateRoot (see above) so that
|
|
556
556
|
// NavigationProvider has correct values during hydration. If the hydration
|
package/src/client/router.ts
CHANGED
|
@@ -222,16 +222,23 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
222
222
|
* useOptimistic for the pending URL. In tests, the payload is rendered directly.
|
|
223
223
|
*/
|
|
224
224
|
async function renderViaTransition(
|
|
225
|
-
|
|
225
|
+
url: string,
|
|
226
226
|
perform: () => Promise<FetchResult>
|
|
227
227
|
): Promise<HeadElement[] | null> {
|
|
228
228
|
if (deps.navigateTransition) {
|
|
229
229
|
let headElements: HeadElement[] | null = null;
|
|
230
|
-
await deps.navigateTransition(
|
|
230
|
+
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
231
231
|
const result = await perform();
|
|
232
232
|
headElements = result.headElements;
|
|
233
233
|
// Merge partial payload with cached segments before wrapping
|
|
234
234
|
const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
|
|
235
|
+
// Store the MERGED payload in history — not the partial pre-merge tree.
|
|
236
|
+
// This ensures handlePopState replays the complete tree on back/forward.
|
|
237
|
+
historyStack.push(url, {
|
|
238
|
+
payload: merged,
|
|
239
|
+
headElements: result.headElements,
|
|
240
|
+
params: result.params,
|
|
241
|
+
});
|
|
235
242
|
return wrapPayload(merged);
|
|
236
243
|
});
|
|
237
244
|
return headElements;
|
|
@@ -240,6 +247,12 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
240
247
|
const result = await perform();
|
|
241
248
|
// Merge partial payload with cached segments before rendering
|
|
242
249
|
const merged = mergeAndCachePayload(result.payload, result.skippedSegments);
|
|
250
|
+
// Store merged payload in history
|
|
251
|
+
historyStack.push(url, {
|
|
252
|
+
payload: merged,
|
|
253
|
+
headElements: result.headElements,
|
|
254
|
+
params: result.params,
|
|
255
|
+
});
|
|
243
256
|
renderPayload(merged);
|
|
244
257
|
return result.headElements;
|
|
245
258
|
}
|
|
@@ -284,7 +297,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
284
297
|
if (result === undefined) {
|
|
285
298
|
// Fetch RSC payload with state tree for partial rendering.
|
|
286
299
|
// Send current URL for intercepting route resolution (modal pattern).
|
|
287
|
-
const stateTree = segmentCache.serializeStateTree();
|
|
300
|
+
const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
|
|
288
301
|
const rawCurrentUrl = deps.getCurrentUrl();
|
|
289
302
|
const currentUrl = rawCurrentUrl.startsWith('http')
|
|
290
303
|
? new URL(rawCurrentUrl).pathname
|
|
@@ -299,12 +312,10 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
299
312
|
deps.pushState({ timber: true, scrollY: 0 }, '', url);
|
|
300
313
|
}
|
|
301
314
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
params: result.params,
|
|
307
|
-
});
|
|
315
|
+
// NOTE: History push is deferred — the merged payload (after segment
|
|
316
|
+
// merging in renderViaTransition) is stored by the caller, not here.
|
|
317
|
+
// Storing result.payload here would record the partial (pre-merge)
|
|
318
|
+
// RSC tree, causing handlePopState to replay an incomplete tree.
|
|
308
319
|
|
|
309
320
|
// Update the segment cache with the new route's segment tree.
|
|
310
321
|
updateSegmentCache(result.segmentInfo);
|
|
@@ -375,11 +386,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
375
386
|
const headElements = await renderViaTransition(currentUrl, async () => {
|
|
376
387
|
// No state tree sent — server renders the complete RSC payload
|
|
377
388
|
const result = await fetchRscPayload(currentUrl, deps);
|
|
378
|
-
|
|
379
|
-
payload: result.payload,
|
|
380
|
-
headElements: result.headElements,
|
|
381
|
-
params: result.params,
|
|
382
|
-
});
|
|
389
|
+
// History push handled by renderViaTransition (stores merged payload)
|
|
383
390
|
updateSegmentCache(result.segmentInfo);
|
|
384
391
|
updateNavigationState(result.params, currentUrl);
|
|
385
392
|
return result;
|
|
@@ -414,15 +421,11 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
414
421
|
setPending(true, url);
|
|
415
422
|
try {
|
|
416
423
|
const headElements = await renderViaTransition(url, async () => {
|
|
417
|
-
const stateTree = segmentCache.serializeStateTree();
|
|
424
|
+
const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
|
|
418
425
|
const result = await fetchRscPayload(url, deps, stateTree);
|
|
419
426
|
updateSegmentCache(result.segmentInfo);
|
|
420
427
|
updateNavigationState(result.params, url);
|
|
421
|
-
|
|
422
|
-
payload: result.payload,
|
|
423
|
-
headElements: result.headElements,
|
|
424
|
-
params: result.params,
|
|
425
|
-
});
|
|
428
|
+
// History push handled by renderViaTransition (stores merged payload)
|
|
426
429
|
return result;
|
|
427
430
|
});
|
|
428
431
|
|
|
@@ -447,7 +450,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
447
450
|
if (historyStack.has(url)) return;
|
|
448
451
|
|
|
449
452
|
// Fire-and-forget fetch
|
|
450
|
-
const stateTree = segmentCache.serializeStateTree();
|
|
453
|
+
const stateTree = segmentCache.serializeStateTree(segmentElementCache.getMergeablePaths());
|
|
451
454
|
void fetchRscPayload(url, deps, stateTree).then(
|
|
452
455
|
(result) => {
|
|
453
456
|
prefetchCache.set(url, result);
|
|
@@ -72,25 +72,34 @@ export class SegmentCache {
|
|
|
72
72
|
* Only includes sync segments — async segments are excluded because the
|
|
73
73
|
* server must always re-render them (they may depend on request context).
|
|
74
74
|
*
|
|
75
|
+
* When mergeableFilter is provided, only segments whose paths are in the
|
|
76
|
+
* set are included. This ensures the server only skips segments that the
|
|
77
|
+
* client can actually merge (i.e., segments whose cached element tree
|
|
78
|
+
* contains an inner SegmentProvider the merger can splice into).
|
|
79
|
+
*
|
|
75
80
|
* This is a performance optimization only, NOT a security boundary.
|
|
76
81
|
* The server always runs all access.ts files regardless of the state tree.
|
|
77
82
|
*/
|
|
78
|
-
serializeStateTree(): StateTree {
|
|
83
|
+
serializeStateTree(mergeableFilter?: Set<string>): StateTree {
|
|
79
84
|
const segments: string[] = [];
|
|
80
85
|
if (this.root) {
|
|
81
|
-
collectSyncSegments(this.root, segments);
|
|
86
|
+
collectSyncSegments(this.root, segments, mergeableFilter);
|
|
82
87
|
}
|
|
83
88
|
return { segments };
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/** Recursively collect sync segment paths from the tree */
|
|
88
|
-
function collectSyncSegments(
|
|
89
|
-
|
|
93
|
+
function collectSyncSegments(
|
|
94
|
+
node: SegmentNode,
|
|
95
|
+
out: string[],
|
|
96
|
+
mergeableFilter?: Set<string>
|
|
97
|
+
): void {
|
|
98
|
+
if (!node.isAsync && (!mergeableFilter || mergeableFilter.has(node.segment))) {
|
|
90
99
|
out.push(node.segment);
|
|
91
100
|
}
|
|
92
101
|
for (const child of node.children.values()) {
|
|
93
|
-
collectSyncSegments(child, out);
|
|
102
|
+
collectSyncSegments(child, out, mergeableFilter);
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -37,6 +37,13 @@ export function useSegmentContext(): SegmentContextValue | null {
|
|
|
37
37
|
|
|
38
38
|
interface SegmentProviderProps {
|
|
39
39
|
segments: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Unique identifier for this segment, used by the client-side segment
|
|
42
|
+
* merger for element caching. For route groups this includes the group
|
|
43
|
+
* name (e.g., "/(marketing)") since groups share their parent's urlPath.
|
|
44
|
+
* Falls back to the reconstructed path from `segments` if not provided.
|
|
45
|
+
*/
|
|
46
|
+
segmentId?: string;
|
|
40
47
|
parallelRouteKeys: string[];
|
|
41
48
|
children: React.ReactNode;
|
|
42
49
|
}
|
|
@@ -45,7 +52,7 @@ interface SegmentProviderProps {
|
|
|
45
52
|
* Wraps each layout to provide segment position context.
|
|
46
53
|
* Injected by rsc-entry.ts during element tree construction.
|
|
47
54
|
*/
|
|
48
|
-
export function SegmentProvider({ segments, parallelRouteKeys, children }: SegmentProviderProps) {
|
|
55
|
+
export function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, children }: SegmentProviderProps) {
|
|
49
56
|
const value = useMemo(
|
|
50
57
|
() => ({ segments, parallelRouteKeys }),
|
|
51
58
|
// segments and parallelRouteKeys are static per layout — they don't change
|
|
@@ -34,6 +34,13 @@ export interface CachedSegmentEntry {
|
|
|
34
34
|
segmentPath: string;
|
|
35
35
|
/** The SegmentProvider element for this segment */
|
|
36
36
|
element: ReactElement;
|
|
37
|
+
/**
|
|
38
|
+
* Whether this segment's cached element contains a nested SegmentProvider.
|
|
39
|
+
* Only segments with inner SegmentProviders are safe to skip — the merger
|
|
40
|
+
* can only replace inner SegmentProviders, not pages embedded in layout output.
|
|
41
|
+
* Used by the state tree serialization to exclude non-mergeable segments.
|
|
42
|
+
*/
|
|
43
|
+
hasMergeableChild: boolean;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// ─── Segment Element Cache ───────────────────────────────────────
|
|
@@ -64,6 +71,29 @@ export class SegmentElementCache {
|
|
|
64
71
|
get size(): number {
|
|
65
72
|
return this.entries.size;
|
|
66
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the set of segment paths that are safe for the server to skip.
|
|
77
|
+
* Only segments with an inner SegmentProvider (hasMergeableChild) are
|
|
78
|
+
* included — the merger can only replace inner SegmentProviders, not
|
|
79
|
+
* pages embedded in layout output. Used to filter the state tree.
|
|
80
|
+
*
|
|
81
|
+
* Returns an empty set if the element cache is empty (no elements
|
|
82
|
+
* cached yet). This is the safe default — an empty set means no
|
|
83
|
+
* segments pass the filter, so the state tree is empty and the server
|
|
84
|
+
* does a full render. The element cache is populated lazily after the
|
|
85
|
+
* first SPA navigation (RSC-decoded elements from hydration are
|
|
86
|
+
* thenables that can't be walked until React resolves them).
|
|
87
|
+
*/
|
|
88
|
+
getMergeablePaths(): Set<string> {
|
|
89
|
+
const paths = new Set<string>();
|
|
90
|
+
for (const [, entry] of this.entries) {
|
|
91
|
+
if (entry.hasMergeableChild) {
|
|
92
|
+
paths.add(entry.segmentPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return paths;
|
|
96
|
+
}
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
// ─── SegmentProvider Detection ───────────────────────────────────
|
|
@@ -81,12 +111,16 @@ export function isSegmentProvider(element: unknown): element is ReactElement {
|
|
|
81
111
|
|
|
82
112
|
/**
|
|
83
113
|
* Extract the segment path from a SegmentProvider element.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
114
|
+
*
|
|
115
|
+
* Uses the `segmentId` prop if available (set by the server for route groups
|
|
116
|
+
* to distinguish siblings that share the same urlPath). Falls back to
|
|
117
|
+
* reconstructing from the `segments` array prop.
|
|
86
118
|
*/
|
|
87
119
|
export function getSegmentPath(element: ReactElement): string {
|
|
88
|
-
const
|
|
89
|
-
|
|
120
|
+
const props = element.props as { segments: string[]; segmentId?: string };
|
|
121
|
+
// segmentId is the authoritative key — includes group name for route groups
|
|
122
|
+
if (props.segmentId) return props.segmentId;
|
|
123
|
+
const filtered = props.segments.filter(Boolean);
|
|
90
124
|
return filtered.length === 0 ? '/' : '/' + filtered.join('/');
|
|
91
125
|
}
|
|
92
126
|
|
|
@@ -102,6 +136,12 @@ export function getSegmentPath(element: ReactElement): string {
|
|
|
102
136
|
export function extractSegments(element: unknown): CachedSegmentEntry[] {
|
|
103
137
|
const segments: CachedSegmentEntry[] = [];
|
|
104
138
|
walkForSegments(element, segments);
|
|
139
|
+
// Compute hasMergeableChild: a segment is mergeable if there's another
|
|
140
|
+
// SegmentProvider nested below it. The segments list is ordered outermost
|
|
141
|
+
// to innermost, so each segment's child is the next entry.
|
|
142
|
+
for (let i = 0; i < segments.length; i++) {
|
|
143
|
+
segments[i].hasMergeableChild = i < segments.length - 1;
|
|
144
|
+
}
|
|
105
145
|
return segments;
|
|
106
146
|
}
|
|
107
147
|
|
|
@@ -117,6 +157,7 @@ function walkForSegments(node: unknown, out: CachedSegmentEntry[]): void {
|
|
|
117
157
|
out.push({
|
|
118
158
|
segmentPath: getSegmentPath(el),
|
|
119
159
|
element: el,
|
|
160
|
+
hasMergeableChild: false, // computed after collection in extractSegments
|
|
120
161
|
});
|
|
121
162
|
// Continue walking into children to find nested segments
|
|
122
163
|
walkChildren(props.children as ReactNode, out);
|
|
@@ -224,9 +265,16 @@ export function replaceInnerSegment(
|
|
|
224
265
|
const path = findSegmentProviderPath(cachedElement, innerSegmentPath);
|
|
225
266
|
|
|
226
267
|
if (!path || path.length === 0) {
|
|
227
|
-
// No inner SegmentProvider found —
|
|
228
|
-
//
|
|
229
|
-
|
|
268
|
+
// No inner SegmentProvider found — this segment's cached element
|
|
269
|
+
// wraps a page directly (no child layout with a SegmentProvider).
|
|
270
|
+
// We CANNOT safely replace the page because it's embedded deep in
|
|
271
|
+
// the layout's server-rendered output tree and we don't know its
|
|
272
|
+
// position. Return the cached element unchanged as a safety fallback.
|
|
273
|
+
//
|
|
274
|
+
// The server should not skip segments without a child layout below
|
|
275
|
+
// them (enforced by hasRenderedLayoutBelow in buildRouteElement).
|
|
276
|
+
// If this codepath is reached, it indicates a server/client mismatch.
|
|
277
|
+
return cachedElement;
|
|
230
278
|
}
|
|
231
279
|
|
|
232
280
|
// Reconstruct bottom-up: replace the innermost element first, then
|
|
@@ -326,6 +326,16 @@ export async function buildRouteElement(
|
|
|
326
326
|
// client navigation), sync layouts the client already has are skipped.
|
|
327
327
|
// Access.ts already ran for ALL segments in the pre-render loop above.
|
|
328
328
|
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
329
|
+
//
|
|
330
|
+
// hasRenderedLayoutBelow tracks whether a non-skipped layout has been
|
|
331
|
+
// seen below the current segment. A segment can ONLY be skipped if
|
|
332
|
+
// there is a rendered layout below it — the client merger can only
|
|
333
|
+
// replace inner SegmentProviders (client component boundaries), not
|
|
334
|
+
// page content embedded in a layout's server-rendered output.
|
|
335
|
+
// Without this guard, skipping the innermost layout causes the merger
|
|
336
|
+
// to drop the layout entirely and replace it with just the page.
|
|
337
|
+
let hasRenderedLayoutBelow = false;
|
|
338
|
+
|
|
329
339
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
330
340
|
const segment = segments[i];
|
|
331
341
|
const isLeaf = i === segments.length - 1;
|
|
@@ -334,12 +344,22 @@ export async function buildRouteElement(
|
|
|
334
344
|
// Check if this segment's layout can be skipped for partial rendering.
|
|
335
345
|
// Skipped segments: no layout wrapping, no error boundaries, no slots,
|
|
336
346
|
// no AccessGate in element tree (access already ran pre-render).
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
)
|
|
347
|
+
//
|
|
348
|
+
// Additional constraints beyond shouldSkipSegment:
|
|
349
|
+
// - Must have a rendered layout below (so the merger can find an
|
|
350
|
+
// inner SegmentProvider to splice the new content into)
|
|
351
|
+
// - Route groups are never skipped because sibling groups share the
|
|
352
|
+
// same urlPath (e.g., /(marketing) and /(app) both have "/"),
|
|
353
|
+
// which would cause the wrong cached layout to be reused
|
|
354
|
+
const skip =
|
|
355
|
+
shouldSkipSegment(
|
|
356
|
+
segment.urlPath,
|
|
357
|
+
layoutComponent,
|
|
358
|
+
isLeaf,
|
|
359
|
+
clientStateTree ?? null
|
|
360
|
+
) &&
|
|
361
|
+
hasRenderedLayoutBelow &&
|
|
362
|
+
segment.segmentType !== 'group';
|
|
343
363
|
|
|
344
364
|
if (skip) {
|
|
345
365
|
// Skip this segment entirely — the client uses its cached version.
|
|
@@ -350,6 +370,12 @@ export async function buildRouteElement(
|
|
|
350
370
|
continue;
|
|
351
371
|
}
|
|
352
372
|
|
|
373
|
+
// This segment is rendered — mark that future (outer) segments have
|
|
374
|
+
// a rendered layout below them and can safely be skipped.
|
|
375
|
+
if (layoutComponent) {
|
|
376
|
+
hasRenderedLayoutBelow = true;
|
|
377
|
+
}
|
|
378
|
+
|
|
353
379
|
// Wrap with error boundaries from this segment (inside layout).
|
|
354
380
|
element = await wrapSegmentWithErrorBoundaries(segment, element, h);
|
|
355
381
|
|
|
@@ -407,8 +433,18 @@ export async function buildRouteElement(
|
|
|
407
433
|
);
|
|
408
434
|
};
|
|
409
435
|
|
|
436
|
+
// segmentId uniquely identifies this segment for client-side element
|
|
437
|
+
// caching. For route groups, urlPath is shared with the parent (both "/"),
|
|
438
|
+
// so we include the group name to distinguish them. Without this, the
|
|
439
|
+
// segment merger's element cache would conflate root and group elements.
|
|
440
|
+
const segmentId =
|
|
441
|
+
segment.segmentType === 'group'
|
|
442
|
+
? `${segment.urlPath === '/' ? '' : segment.urlPath}/${segment.segmentName}`
|
|
443
|
+
: segment.urlPath;
|
|
444
|
+
|
|
410
445
|
element = h(SegmentProvider, {
|
|
411
446
|
segments: segmentPath,
|
|
447
|
+
segmentId,
|
|
412
448
|
parallelRouteKeys,
|
|
413
449
|
children: h(TracedLayout, {
|
|
414
450
|
...slotProps,
|
|
@@ -55,23 +55,23 @@ export function parseClientStateTree(req: Request): Set<string> | null {
|
|
|
55
55
|
* @param clientSegments - Set of paths from X-Timber-State-Tree, or null
|
|
56
56
|
*/
|
|
57
57
|
export function shouldSkipSegment(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
_urlPath: string,
|
|
59
|
+
_layoutComponent: ((...args: unknown[]) => unknown) | undefined,
|
|
60
|
+
_isLeaf: boolean,
|
|
61
|
+
_clientSegments: Set<string> | null
|
|
62
62
|
): boolean {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
return
|
|
63
|
+
// DISABLED: Client-side segment merging via React element tree walking
|
|
64
|
+
// is too fragile for production. The merger's replaceInnerSegment relies
|
|
65
|
+
// on walking RSC-decoded element trees to find SegmentProvider boundaries,
|
|
66
|
+
// but production trees contain structures the walker can't handle:
|
|
67
|
+
// Suspense wrappers, error boundaries, AccessGate components, React lazy
|
|
68
|
+
// refs, and client component module references.
|
|
69
|
+
//
|
|
70
|
+
// The merger infrastructure (segment-merger.ts, element cache, state tree
|
|
71
|
+
// filtering) is in place and tested. Re-enable when the merger can handle
|
|
72
|
+
// real RSC element trees — likely requires a SegmentOutlet client component
|
|
73
|
+
// approach instead of post-hoc element tree walking.
|
|
74
|
+
//
|
|
75
|
+
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
76
|
+
return false;
|
|
77
77
|
}
|