@@ -7,6 +7,7 @@ import type {
7
7
Segment ,
8
8
} from '../server/app-render/types'
9
9
import type { HeadData } from '../shared/lib/app-router-context.shared-runtime'
10
+ import { PAGE_SEGMENT_KEY } from '../shared/lib/segment'
10
11
11
12
export type NormalizedFlightData = {
12
13
/**
@@ -76,3 +77,95 @@ export function normalizeFlightData(
76
77
77
78
return flightData . map ( getFlightDataPartsFromPath )
78
79
}
80
+
81
+ /**
82
+ * This function is used to prepare the flight router state for the request.
83
+ * It removes markers that are not needed by the server, and are purely used
84
+ * for stashing state on the client.
85
+ * @param flightRouterState - The flight router state to prepare.
86
+ * @param isHmrRefresh - Whether this is an HMR refresh request.
87
+ * @returns The prepared flight router state.
88
+ */
89
+ export function prepareFlightRouterStateForRequest (
90
+ flightRouterState : FlightRouterState ,
91
+ isHmrRefresh ?: boolean
92
+ ) : string {
93
+ // HMR requests need the complete, unmodified state for proper functionality
94
+ if ( isHmrRefresh ) {
95
+ return encodeURIComponent ( JSON . stringify ( flightRouterState ) )
96
+ }
97
+
98
+ return encodeURIComponent (
99
+ JSON . stringify ( stripClientOnlyDataFromFlightRouterState ( flightRouterState ) )
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Recursively strips client-only data from FlightRouterState while preserving
105
+ * server-needed information for proper rendering decisions.
106
+ */
107
+ function stripClientOnlyDataFromFlightRouterState (
108
+ flightRouterState : FlightRouterState
109
+ ) : FlightRouterState {
110
+ const [
111
+ segment ,
112
+ parallelRoutes ,
113
+ _url , // Intentionally unused - URLs are client-only
114
+ refreshMarker ,
115
+ isRootLayout ,
116
+ hasLoadingBoundary ,
117
+ ] = flightRouterState
118
+
119
+ // __PAGE__ segments are always fetched from the server, so there's
120
+ // no need to send them up
121
+ const cleanedSegment = stripSearchParamsFromPageSegment ( segment )
122
+
123
+ // Recursively process parallel routes
124
+ const cleanedParallelRoutes : { [ key : string ] : FlightRouterState } = { }
125
+ for ( const [ key , childState ] of Object . entries ( parallelRoutes ) ) {
126
+ cleanedParallelRoutes [ key ] =
127
+ stripClientOnlyDataFromFlightRouterState ( childState )
128
+ }
129
+
130
+ const result : FlightRouterState = [
131
+ cleanedSegment ,
132
+ cleanedParallelRoutes ,
133
+ null , // URLs omitted - server reconstructs paths from segments
134
+ shouldPreserveRefreshMarker ( refreshMarker ) ? refreshMarker : null ,
135
+ ]
136
+
137
+ // Append optional fields if present
138
+ if ( isRootLayout !== undefined ) {
139
+ result [ 4 ] = isRootLayout
140
+ }
141
+ if ( hasLoadingBoundary !== undefined ) {
142
+ result [ 5 ] = hasLoadingBoundary
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ /**
149
+ * Strips search parameters from __PAGE__ segments to prevent sensitive
150
+ * client-side data from being sent to the server.
151
+ */
152
+ function stripSearchParamsFromPageSegment ( segment : Segment ) : Segment {
153
+ if (
154
+ typeof segment === 'string' &&
155
+ segment . startsWith ( PAGE_SEGMENT_KEY + '?' )
156
+ ) {
157
+ return PAGE_SEGMENT_KEY
158
+ }
159
+ return segment
160
+ }
161
+
162
+ /**
163
+ * Determines whether the refresh marker should be sent to the server
164
+ * Client-only markers like 'refresh' are stripped, while server-needed markers
165
+ * like 'refetch' and 'inside-shared-layout' are preserved.
166
+ */
167
+ function shouldPreserveRefreshMarker (
168
+ refreshMarker : FlightRouterState [ 3 ]
169
+ ) : boolean {
170
+ return Boolean ( refreshMarker && refreshMarker !== 'refresh' )
171
+ }
0 commit comments