@tanstack/start-server-core 1.167.30 → 1.168.1

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.
@@ -1,6 +1,8 @@
1
1
  import { createMemoryHistory } from '@tanstack/history'
2
2
  import {
3
+ createCsrfMiddleware,
3
4
  createNullProtoObject,
5
+ csrfSymbol,
4
6
  flattenMiddlewares,
5
7
  mergeHeaders,
6
8
  safeObjectMerge,
@@ -22,19 +24,11 @@ import {
22
24
  import { requestHandler } from './request-response'
23
25
  import { getStartManifest } from './router-manifest'
24
26
  import { handleServerAction } from './server-functions-handler'
27
+ import { createEarlyHintsCollector } from './early-hints'
25
28
  import {
26
- adaptTransformAssetUrlsConfigToTransformAssets,
27
- buildManifestWithClientEntry,
28
- resolveTransformAssetsConfig,
29
- transformManifestAssets,
30
- } from './transformAssetUrls'
31
- import {
32
- collectDynamicHintsFromMatches,
33
- collectStaticHintsFromManifest,
34
- createEarlyHintsEvent,
35
- createResponseLinkHeaderEntries,
36
- getResponseLinkHeaderEntries,
37
- } from './early-hints'
29
+ createCachedBaseManifestLoader,
30
+ createFinalManifestResolver,
31
+ } from './finalManifest'
38
32
 
39
33
  import { HEADERS } from './constants'
40
34
  import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
@@ -48,29 +42,14 @@ import type {
48
42
  StartEntry,
49
43
  } from '@tanstack/start-client-core'
50
44
  import type { RequestHandler } from './request-handler'
51
- import type {
52
- EarlyHint,
53
- EarlyHintsEvent,
54
- EarlyHintsPhase,
55
- OnEarlyHints,
56
- ResponseLinkHeaderEntry,
57
- ResponseLinkHeaderFilter,
58
- ResponseLinkHeaderOptions,
59
- } from './early-hints'
60
45
  import type {
61
46
  AnyRoute,
62
47
  AnyRouter,
63
48
  AnySerializationAdapter,
64
- Manifest,
65
49
  Register,
66
50
  } from '@tanstack/router-core'
67
51
  import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
68
- import type {
69
- StartManifestWithClientEntry,
70
- TransformAssetUrls,
71
- TransformAssets,
72
- TransformAssetsFn,
73
- } from './transformAssetUrls'
52
+ import type { FinalManifestOptions } from './finalManifest'
74
53
 
75
54
  type TODO = any
76
55
 
@@ -78,139 +57,8 @@ type AnyMiddlewareServerFn =
78
57
  | AnyRequestMiddleware['options']['server']
79
58
  | AnyFunctionMiddleware['options']['server']
80
59
 
81
- export interface CreateStartHandlerOptions {
60
+ export interface CreateStartHandlerOptions extends FinalManifestOptions {
82
61
  handler: HandlerCallback<AnyRouter>
83
- /**
84
- * Transform asset URLs and attributes at runtime, e.g. to prepend a CDN prefix.
85
- *
86
- * **String** — a URL prefix prepended to every asset URL (cached by default):
87
- * ```ts
88
- * createStartHandler({
89
- * handler: defaultStreamHandler,
90
- * transformAssets: 'https://cdn.example.com',
91
- * })
92
- * ```
93
- *
94
- * **Object shorthand** — a URL prefix with optional `crossOrigin`:
95
- * ```ts
96
- * createStartHandler({
97
- * handler: defaultStreamHandler,
98
- * transformAssets: {
99
- * prefix: 'https://cdn.example.com',
100
- * crossOrigin: 'anonymous',
101
- * },
102
- * })
103
- * ```
104
- *
105
- * `crossOrigin` accepts a single value or a per-kind record:
106
- * ```ts
107
- * transformAssets: {
108
- * prefix: 'https://cdn.example.com',
109
- * crossOrigin: {
110
- * modulepreload: 'anonymous',
111
- * stylesheet: 'use-credentials',
112
- * },
113
- * }
114
- * ```
115
- *
116
- * **Callback** — receives `{ kind, url }` and returns either a string URL or
117
- * `{ href, crossOrigin? }` (cached by default — runs once on first request):
118
- * ```ts
119
- * createStartHandler({
120
- * handler: defaultStreamHandler,
121
- * transformAssets: ({ kind, url }) => {
122
- * const href = `https://cdn.example.com${url}`
123
- *
124
- * if (kind === 'modulepreload') {
125
- * return { href, crossOrigin: 'anonymous' }
126
- * }
127
- *
128
- * return { href }
129
- * },
130
- * })
131
- * ```
132
- *
133
- * **Object** — for explicit cache control:
134
- * ```ts
135
- * createStartHandler({
136
- * handler: defaultStreamHandler,
137
- * transformAssets: {
138
- * transform: ({ url }) => {
139
- * const region = getRequest().headers.get('x-region') || 'us'
140
- * return { href: `https://cdn-${region}.example.com${url}` }
141
- * },
142
- * cache: false,
143
- * },
144
- * })
145
- * ```
146
- *
147
- * `kind` is one of `'modulepreload' | 'stylesheet' | 'clientEntry'`.
148
- * `crossOrigin` applies to manifest-managed `<link>` assets.
149
- *
150
- * By default, the transformed manifest is cached after the first request
151
- * (`cache: true`). Set `cache: false` for per-request transforms.
152
- *
153
- * If you're using a cached transform, you can optionally set `warmup: true`
154
- * (object form only) to compute the transformed manifest in the background at
155
- * server startup.
156
- *
157
- * Note: This only transforms URLs managed by TanStack Start's manifest
158
- * (JS preloads, CSS links, and the client entry script). For asset imports
159
- * used directly in components (e.g. `import logo from './logo.svg'`),
160
- * configure Vite's `experimental.renderBuiltUrl` in your vite.config.ts.
161
- */
162
- transformAssets?: TransformAssets
163
- /**
164
- * @deprecated Use `transformAssets` instead.
165
- *
166
- * **String** — a URL prefix prepended to every asset URL (cached by default):
167
- * ```ts
168
- * createStartHandler({
169
- * handler: defaultStreamHandler,
170
- * transformAssetUrls: 'https://cdn.example.com',
171
- * })
172
- * ```
173
- *
174
- * **Callback** — receives `{ url, type }` and returns a new URL
175
- * (cached by default — runs once on first request):
176
- * ```ts
177
- * createStartHandler({
178
- * handler: defaultStreamHandler,
179
- * transformAssetUrls: ({ url, type }) => {
180
- * return `https://cdn.example.com${url}`
181
- * },
182
- * })
183
- * ```
184
- *
185
- * **Object** — for explicit cache control:
186
- * ```ts
187
- * createStartHandler({
188
- * handler: defaultStreamHandler,
189
- * transformAssetUrls: {
190
- * transform: ({ url }) => {
191
- * const region = getRequest().headers.get('x-region') || 'us'
192
- * return `https://cdn-${region}.example.com${url}`
193
- * },
194
- * cache: false, // transform per-request
195
- * },
196
- * })
197
- * ```
198
- *
199
- * `type` is one of `'modulepreload' | 'stylesheet' | 'clientEntry'`.
200
- *
201
- * By default, the transformed manifest is cached after the first request
202
- * (`cache: true`). Set `cache: false` for per-request transforms.
203
- *
204
- * If you're using a cached transform, you can optionally set `warmup: true`
205
- * (object form only) to compute the transformed manifest in the background at
206
- * server startup.
207
- *
208
- * Note: This only transforms URLs managed by TanStack Start's manifest
209
- * (JS preloads, CSS links, and the client entry script). For asset imports
210
- * used directly in components (e.g. `import logo from './logo.svg'`),
211
- * configure Vite's `experimental.renderBuiltUrl` in your vite.config.ts.
212
- */
213
- transformAssetUrls?: TransformAssetUrls
214
62
  }
215
63
 
216
64
  function getStartResponseHeaders(opts: { router: AnyRouter }) {
@@ -225,106 +73,6 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
225
73
  return headers
226
74
  }
227
75
 
228
- function notifyEarlyHints(
229
- phase: EarlyHintsPhase,
230
- event: EarlyHintsEvent,
231
- onEarlyHints: OnEarlyHints,
232
- ) {
233
- try {
234
- const result = onEarlyHints(event)
235
- if (result) {
236
- void Promise.resolve(result).catch((err) => {
237
- console.error(`Error sending ${phase} early hints:`, err)
238
- })
239
- }
240
- } catch (err) {
241
- console.error(`Error sending ${phase} early hints:`, err)
242
- }
243
- }
244
-
245
- function getResponseLinkHeaderFilter(
246
- responseLinkHeader: boolean | ResponseLinkHeaderOptions | undefined,
247
- ): ResponseLinkHeaderFilter | undefined {
248
- if (typeof responseLinkHeader !== 'object') {
249
- return undefined
250
- }
251
-
252
- return responseLinkHeader.filter
253
- }
254
-
255
- function appendResponseLinkHeaders(opts: {
256
- responseHeaders: Headers
257
- entries: ReadonlyArray<ResponseLinkHeaderEntry>
258
- filter?: ResponseLinkHeaderFilter
259
- }) {
260
- if (!opts.filter) {
261
- for (const entry of opts.entries) {
262
- opts.responseHeaders.append('Link', entry.link)
263
- }
264
- return
265
- }
266
-
267
- const links = getResponseLinkHeaderEntries(opts)
268
-
269
- for (const link of links) {
270
- opts.responseHeaders.append('Link', link)
271
- }
272
- }
273
-
274
- function collectResponseLinkHeaderEntries(opts: {
275
- phase: EarlyHintsPhase
276
- event: EarlyHintsEvent
277
- entries: Array<ResponseLinkHeaderEntry>
278
- }) {
279
- for (let index = 0; index < opts.event.hints.length; index++) {
280
- opts.entries.push({
281
- phase: opts.phase,
282
- hint: opts.event.hints[index]!,
283
- link: opts.event.links[index]!,
284
- })
285
- }
286
- }
287
-
288
- function handleCollectedEarlyHints(opts: {
289
- phase: EarlyHintsPhase
290
- hints: ReadonlyArray<EarlyHint>
291
- sentLinks: Set<string>
292
- sentHints?: Array<EarlyHint>
293
- onEarlyHints?: OnEarlyHints
294
- responseLinkHeaderEntries?: Array<ResponseLinkHeaderEntry>
295
- }) {
296
- const event = opts.onEarlyHints
297
- ? createEarlyHintsEvent({
298
- phase: opts.phase,
299
- hints: opts.hints,
300
- sentLinks: opts.sentLinks,
301
- sentHints: opts.sentHints!,
302
- })
303
- : undefined
304
-
305
- if (event) {
306
- notifyEarlyHints(opts.phase, event, opts.onEarlyHints!)
307
- }
308
-
309
- if (!opts.responseLinkHeaderEntries) return
310
-
311
- if (event) {
312
- collectResponseLinkHeaderEntries({
313
- phase: opts.phase,
314
- event,
315
- entries: opts.responseLinkHeaderEntries,
316
- })
317
- return
318
- }
319
-
320
- createResponseLinkHeaderEntries({
321
- phase: opts.phase,
322
- hints: opts.hints,
323
- sentLinks: opts.sentLinks,
324
- entries: opts.responseLinkHeaderEntries,
325
- })
326
- }
327
-
328
76
  interface PluginAdaptersEntry {
329
77
  hasPluginAdapters: boolean
330
78
  pluginSerializationAdapters: Array<AnySerializationAdapter>
@@ -339,13 +87,21 @@ interface Entries {
339
87
  // Cached entries - promises stored immediately to prevent concurrent imports
340
88
  // that can cause race conditions during module initialization
341
89
  let entriesPromise: Promise<Entries> | undefined
342
- let baseManifestPromise: Promise<StartManifestWithClientEntry> | undefined
343
-
344
- /**
345
- * Cached final manifest (with client entry script tag). In production,
346
- * this is computed once and reused for every request when caching is enabled.
347
- */
348
- let cachedFinalManifestPromise: Promise<Manifest> | undefined
90
+ let hasWarnedMissingCsrfMiddleware = false
91
+ const defaultCsrfMiddleware = createCsrfMiddleware({
92
+ filter: (ctx) => ctx.handlerType === 'serverFn',
93
+ })
94
+ const getCachedBaseManifest = createCachedBaseManifestLoader(() =>
95
+ getStartManifest(),
96
+ )
97
+ const getProdBaseManifest: typeof getStartManifest = () =>
98
+ getCachedBaseManifest()
99
+ const getBaseManifest =
100
+ process.env.TSS_DEV_SERVER === 'true' ? getStartManifest : getProdBaseManifest
101
+ const createEarlyHintsForRequest: typeof createEarlyHintsCollector =
102
+ process.env.TSS_DEV_SERVER === 'true'
103
+ ? () => undefined
104
+ : createEarlyHintsCollector
349
105
 
350
106
  async function loadEntries(): Promise<Entries> {
351
107
  const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
@@ -370,59 +126,37 @@ function getEntries() {
370
126
  return entriesPromise
371
127
  }
372
128
 
373
- /**
374
- * Returns the raw manifest data (without client entry script tag baked in).
375
- * In dev mode, always returns fresh data. In prod, cached.
376
- */
377
- function getBaseManifest(
378
- matchedRoutes?: ReadonlyArray<AnyRoute>,
379
- ): Promise<StartManifestWithClientEntry> {
380
- // In dev mode, always get fresh manifest (no caching) to include route-specific dev styles
381
- if (process.env.TSS_DEV_SERVER === 'true') {
382
- return getStartManifest(matchedRoutes)
383
- }
384
- // In prod, cache the base manifest
385
- if (!baseManifestPromise) {
386
- baseManifestPromise = getStartManifest()
387
- }
388
- return baseManifestPromise
129
+ function hasCsrfMiddleware(
130
+ middlewares: Array<AnyRequestMiddleware | AnyFunctionMiddleware>,
131
+ ): boolean {
132
+ return middlewares.some((middleware) => csrfSymbol in middleware)
389
133
  }
390
134
 
391
- /**
392
- * Resolves a final Manifest for a given request.
393
- *
394
- * - No transform: builds client entry script tag and returns (cached in prod).
395
- * - Cached transform: transforms all URLs + builds script tag, caches result.
396
- * - Per-request transform: deep-clones base manifest, transforms per-request.
397
- */
398
- async function resolveManifest(
399
- matchedRoutes: ReadonlyArray<AnyRoute> | undefined,
400
- transformFn: TransformAssetsFn | undefined,
401
- cache: boolean,
402
- ): Promise<Manifest> {
403
- const base = await getBaseManifest(matchedRoutes)
404
-
405
- const computeFinalManifest = async () => {
406
- return transformFn
407
- ? await transformManifestAssets(base, transformFn, { clone: !cache })
408
- : buildManifestWithClientEntry(base)
409
- }
135
+ function warnMissingCsrfMiddlewareOnce() {
136
+ if (hasWarnedMissingCsrfMiddleware) return
137
+ hasWarnedMissingCsrfMiddleware = true
410
138
 
411
- // In dev, always compute fresh to include route-specific dev styles.
412
- if (process.env.TSS_DEV_SERVER === 'true') {
413
- return computeFinalManifest()
414
- }
139
+ console.warn(`TanStack Start server functions are not protected by the CSRF middleware.
415
140
 
416
- // In prod, cache unless we're explicitly doing per-request transforms.
417
- if (!transformFn || cache) {
418
- if (!cachedFinalManifestPromise) {
419
- cachedFinalManifestPromise = computeFinalManifest()
420
- }
421
- return cachedFinalManifestPromise
422
- }
141
+ Server functions are same-origin RPC endpoints and should be protected from cross-site requests.
142
+
143
+ Add the CSRF middleware in src/start.ts:
144
+
145
+ const csrfMiddleware = createCsrfMiddleware({
146
+ filter: (ctx) => ctx.handlerType === 'serverFn',
147
+ })
148
+
149
+ export const startInstance = createStart(() => ({
150
+ requestMiddleware: [csrfMiddleware],
151
+ }))
423
152
 
424
- // Per-request transform deep-clone and transform every time.
425
- return computeFinalManifest()
153
+ If you intentionally handle CSRF another way, disable this warning:
154
+
155
+ tanstackStart({
156
+ serverFns: {
157
+ disableCsrfMiddlewareWarning: true,
158
+ },
159
+ })`)
426
160
  }
427
161
 
428
162
  // Pre-computed constants
@@ -569,96 +303,22 @@ function handlerToMiddleware(
569
303
  export function createStartHandler<TRegister = Register>(
570
304
  cbOrOptions: HandlerCallback<AnyRouter> | CreateStartHandlerOptions,
571
305
  ): RequestHandler<TRegister> {
572
- // Normalize the overloaded argument
306
+ const handlerOptions: FinalManifestOptions =
307
+ typeof cbOrOptions === 'function' ? {} : cbOrOptions
573
308
  const cb: HandlerCallback<AnyRouter> =
574
309
  typeof cbOrOptions === 'function' ? cbOrOptions : cbOrOptions.handler
575
- const transformAssetsOption: TransformAssets | undefined =
576
- typeof cbOrOptions === 'function' ? undefined : cbOrOptions.transformAssets
577
- const transformAssetUrlsOption: TransformAssetUrls | undefined =
578
- typeof cbOrOptions === 'function'
579
- ? undefined
580
- : cbOrOptions.transformAssetUrls
581
-
582
- const transformOption =
583
- transformAssetsOption !== undefined
584
- ? resolveTransformAssetsConfig(transformAssetsOption)
585
- : transformAssetUrlsOption !== undefined
586
- ? resolveTransformAssetsConfig(
587
- adaptTransformAssetUrlsConfigToTransformAssets(
588
- transformAssetUrlsOption,
589
- ),
590
- )
591
- : undefined
592
-
593
- const warmupTransformManifest =
594
- (!!transformAssetsOption &&
595
- typeof transformAssetsOption === 'object' &&
596
- 'warmup' in transformAssetsOption &&
597
- transformAssetsOption.warmup === true) ||
598
- (!!transformAssetUrlsOption &&
599
- typeof transformAssetUrlsOption === 'object' &&
600
- transformAssetUrlsOption.warmup === true)
601
-
602
- // Pre-resolve the transform function and cache flag
603
- const resolvedTransformConfig = transformOption
604
- const cache = resolvedTransformConfig ? resolvedTransformConfig.cache : true
605
- const shouldCacheCreateTransform =
606
- cache && process.env.TSS_DEV_SERVER !== 'true'
607
-
608
- // Memoize a single createTransform() result when caching is enabled outside
609
- // of the dev server.
610
- let cachedCreateTransformPromise: Promise<TransformAssetsFn> | undefined
611
-
612
- const getTransformFn = async (
613
- opts: { warmup: true } | { warmup: false; request: Request },
614
- ): Promise<TransformAssetsFn | undefined> => {
615
- if (!resolvedTransformConfig) return undefined
616
-
617
- if (resolvedTransformConfig.type === 'createTransform') {
618
- if (shouldCacheCreateTransform) {
619
- if (!cachedCreateTransformPromise) {
620
- cachedCreateTransformPromise = Promise.resolve(
621
- resolvedTransformConfig.createTransform(opts),
622
- ).catch((error) => {
623
- cachedCreateTransformPromise = undefined
624
- throw error
625
- })
626
- }
627
-
628
- return cachedCreateTransformPromise
629
- }
630
-
631
- return resolvedTransformConfig.createTransform(opts)
632
- }
633
-
634
- return resolvedTransformConfig.transformFn
635
- }
636
-
637
- // Background warmup for cached transforms (production only)
638
- if (
639
- warmupTransformManifest &&
640
- cache &&
641
- process.env.TSS_DEV_SERVER !== 'true' &&
642
- !cachedFinalManifestPromise
643
- ) {
644
- // NOTE: Do not call resolveManifest() here.
645
- // resolveManifest() reads from cachedFinalManifestPromise, and since we set
646
- // cachedFinalManifestPromise to this warmup promise, that would create a
647
- // self-referential promise and hang forever.
648
- const warmupPromise = (async () => {
649
- const base = await getBaseManifest(undefined)
650
- const transformFn = await getTransformFn({ warmup: true })
651
- return transformFn
652
- ? await transformManifestAssets(base, transformFn, { clone: false })
653
- : buildManifestWithClientEntry(base)
654
- })()
655
- cachedFinalManifestPromise = warmupPromise
656
- warmupPromise.catch(() => {
657
- // If warmup fails, allow the next request to retry.
658
- if (cachedFinalManifestPromise === warmupPromise) {
659
- cachedFinalManifestPromise = undefined
660
- }
661
- cachedCreateTransformPromise = undefined
310
+ const finalManifestResolver = createFinalManifestResolver({
311
+ ...handlerOptions,
312
+ cacheCreateTransform: process.env.TSS_DEV_SERVER !== 'true',
313
+ })
314
+ const resolveManifestForRequest =
315
+ process.env.TSS_DEV_SERVER === 'true'
316
+ ? finalManifestResolver.resolveUncached
317
+ : finalManifestResolver.resolveCached
318
+
319
+ if (process.env.TSS_DEV_SERVER !== 'true') {
320
+ finalManifestResolver.warmup({
321
+ getBaseManifest: () => getBaseManifest(undefined),
662
322
  })
663
323
  }
664
324
 
@@ -682,6 +342,7 @@ export function createStartHandler<TRegister = Register>(
682
342
  }
683
343
 
684
344
  const entries = await getEntries()
345
+ const hasStartInstance = !!entries.startEntry.startInstance
685
346
  const startOptions: AnyStartInstanceOptions =
686
347
  (await entries.startEntry.startInstance?.getOptions()) ||
687
348
  ({} as AnyStartInstanceOptions)
@@ -697,12 +358,15 @@ export function createStartHandler<TRegister = Register>(
697
358
 
698
359
  const requestStartOptions = {
699
360
  ...startOptions,
361
+ requestMiddleware: hasStartInstance
362
+ ? startOptions.requestMiddleware
363
+ : [defaultCsrfMiddleware],
700
364
  serializationAdapters,
701
365
  }
702
366
 
703
367
  // Flatten request middlewares once
704
- const flattenedRequestMiddlewares = startOptions.requestMiddleware
705
- ? flattenMiddlewares(startOptions.requestMiddleware)
368
+ const flattenedRequestMiddlewares = requestStartOptions.requestMiddleware
369
+ ? flattenMiddlewares(requestStartOptions.requestMiddleware)
706
370
  : []
707
371
 
708
372
  // Create set for deduplication
@@ -745,6 +409,14 @@ export function createStartHandler<TRegister = Register>(
745
409
 
746
410
  // Check for server function requests first (early exit)
747
411
  if (SERVER_FN_BASE && url.pathname.startsWith(SERVER_FN_BASE)) {
412
+ if (
413
+ process.env.NODE_ENV !== 'production' &&
414
+ process.env.TSS_DISABLE_CSRF_MIDDLEWARE_WARNING !== 'true' &&
415
+ !hasCsrfMiddleware(flattenedRequestMiddlewares)
416
+ ) {
417
+ warnMissingCsrfMiddlewareOnce()
418
+ }
419
+
748
420
  const serverFnId = url.pathname
749
421
  .slice(SERVER_FN_BASE.length)
750
422
  .split('/')[0]
@@ -778,6 +450,7 @@ export function createStartHandler<TRegister = Register>(
778
450
  const ctx = await executeMiddleware([...middlewares, serverFnHandler], {
779
451
  request,
780
452
  pathname: url.pathname,
453
+ handlerType: 'serverFn',
781
454
  context: createNullProtoObject(requestOpts?.context),
782
455
  })
783
456
 
@@ -804,44 +477,18 @@ export function createStartHandler<TRegister = Register>(
804
477
  )
805
478
  }
806
479
 
807
- const manifest = await resolveManifest(
808
- matchedRoutes,
809
- await getTransformFn({ warmup: false, request }),
810
- cache,
811
- )
480
+ const manifest = await resolveManifestForRequest({
481
+ request,
482
+ requestInlineCss: requestOpts?.inlineCss,
483
+ getBaseManifest: () => getBaseManifest(matchedRoutes),
484
+ })
812
485
 
813
- const onEarlyHints = requestOpts?.onEarlyHints
814
- const responseLinkHeader = requestOpts?.responseLinkHeader
815
- const shouldCollectEarlyHints =
816
- process.env.TSS_DEV_SERVER !== 'true' &&
817
- (!!onEarlyHints || !!responseLinkHeader)
818
- const sentEarlyHintLinks = shouldCollectEarlyHints
819
- ? new Set<string>()
820
- : undefined
821
- const sentEarlyHints = onEarlyHints ? new Array<EarlyHint>() : undefined
822
- const responseLinkHeaderEntries =
823
- shouldCollectEarlyHints && responseLinkHeader
824
- ? new Array<ResponseLinkHeaderEntry>()
825
- : undefined
826
- const responseLinkHeaderFilter = shouldCollectEarlyHints
827
- ? getResponseLinkHeaderFilter(responseLinkHeader)
828
- : undefined
486
+ const earlyHints = createEarlyHintsForRequest({
487
+ onEarlyHints: requestOpts?.onEarlyHints,
488
+ responseLinkHeader: requestOpts?.responseLinkHeader,
489
+ })
829
490
 
830
- if (
831
- shouldCollectEarlyHints &&
832
- sentEarlyHintLinks &&
833
- matchedRoutes?.length
834
- ) {
835
- const hints = collectStaticHintsFromManifest(manifest, matchedRoutes)
836
- handleCollectedEarlyHints({
837
- phase: 'static',
838
- hints,
839
- sentLinks: sentEarlyHintLinks,
840
- sentHints: sentEarlyHints,
841
- onEarlyHints,
842
- responseLinkHeaderEntries,
843
- })
844
- }
491
+ earlyHints?.collectStatic({ manifest, matchedRoutes })
845
492
 
846
493
  const routerInstance = await getRouter()
847
494
 
@@ -860,18 +507,7 @@ export function createStartHandler<TRegister = Register>(
860
507
  return routerInstance.state.redirect
861
508
  }
862
509
 
863
- if (shouldCollectEarlyHints && sentEarlyHintLinks) {
864
- const loadedMatches = routerInstance.stores.matches.get()
865
- const hints = collectDynamicHintsFromMatches(loadedMatches)
866
- handleCollectedEarlyHints({
867
- phase: 'dynamic',
868
- hints,
869
- sentLinks: sentEarlyHintLinks,
870
- sentHints: sentEarlyHints,
871
- onEarlyHints,
872
- responseLinkHeaderEntries,
873
- })
874
- }
510
+ earlyHints?.collectDynamic(routerInstance.stores.matches.get())
875
511
 
876
512
  // Pass request-scoped assets to dehydrate for manifest injection
877
513
  const ctx = getStartContext({ throwIfNotFound: false })
@@ -882,13 +518,7 @@ export function createStartHandler<TRegister = Register>(
882
518
  const responseHeaders = getStartResponseHeaders({
883
519
  router: routerInstance,
884
520
  })
885
- if (responseLinkHeaderEntries?.length) {
886
- appendResponseLinkHeaders({
887
- responseHeaders,
888
- entries: responseLinkHeaderEntries,
889
- filter: responseLinkHeaderFilter,
890
- })
891
- }
521
+ earlyHints?.appendResponseHeaders(responseHeaders)
892
522
  cbWillCleanup = true
893
523
 
894
524
  return cb({
@@ -937,6 +567,7 @@ export function createStartHandler<TRegister = Register>(
937
567
  {
938
568
  request,
939
569
  pathname: url.pathname,
570
+ handlerType: 'router',
940
571
  context: createNullProtoObject(requestOpts?.context),
941
572
  },
942
573
  )
@@ -1107,6 +738,7 @@ async function handleServerRoutes({
1107
738
  context,
1108
739
  params: routeParams,
1109
740
  pathname,
741
+ handlerType: 'router',
1110
742
  })
1111
743
 
1112
744
  // RFC 9110 §9.3.2: HEAD must carry the same header fields as GET but no body.