@tanstack/start-server-core 1.168.0 → 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.
@@ -24,19 +24,11 @@ import {
24
24
  import { requestHandler } from './request-response'
25
25
  import { getStartManifest } from './router-manifest'
26
26
  import { handleServerAction } from './server-functions-handler'
27
+ import { createEarlyHintsCollector } from './early-hints'
27
28
  import {
28
- adaptTransformAssetUrlsConfigToTransformAssets,
29
- buildManifestWithClientEntry,
30
- resolveTransformAssetsConfig,
31
- transformManifestAssets,
32
- } from './transformAssetUrls'
33
- import {
34
- collectDynamicHintsFromMatches,
35
- collectStaticHintsFromManifest,
36
- createEarlyHintsEvent,
37
- createResponseLinkHeaderEntries,
38
- getResponseLinkHeaderEntries,
39
- } from './early-hints'
29
+ createCachedBaseManifestLoader,
30
+ createFinalManifestResolver,
31
+ } from './finalManifest'
40
32
 
41
33
  import { HEADERS } from './constants'
42
34
  import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
@@ -50,29 +42,14 @@ import type {
50
42
  StartEntry,
51
43
  } from '@tanstack/start-client-core'
52
44
  import type { RequestHandler } from './request-handler'
53
- import type {
54
- EarlyHint,
55
- EarlyHintsEvent,
56
- EarlyHintsPhase,
57
- OnEarlyHints,
58
- ResponseLinkHeaderEntry,
59
- ResponseLinkHeaderFilter,
60
- ResponseLinkHeaderOptions,
61
- } from './early-hints'
62
45
  import type {
63
46
  AnyRoute,
64
47
  AnyRouter,
65
48
  AnySerializationAdapter,
66
- Manifest,
67
49
  Register,
68
50
  } from '@tanstack/router-core'
69
51
  import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
70
- import type {
71
- StartManifestWithClientEntry,
72
- TransformAssetUrls,
73
- TransformAssets,
74
- TransformAssetsFn,
75
- } from './transformAssetUrls'
52
+ import type { FinalManifestOptions } from './finalManifest'
76
53
 
77
54
  type TODO = any
78
55
 
@@ -80,139 +57,8 @@ type AnyMiddlewareServerFn =
80
57
  | AnyRequestMiddleware['options']['server']
81
58
  | AnyFunctionMiddleware['options']['server']
82
59
 
83
- export interface CreateStartHandlerOptions {
60
+ export interface CreateStartHandlerOptions extends FinalManifestOptions {
84
61
  handler: HandlerCallback<AnyRouter>
85
- /**
86
- * Transform asset URLs and attributes at runtime, e.g. to prepend a CDN prefix.
87
- *
88
- * **String** — a URL prefix prepended to every asset URL (cached by default):
89
- * ```ts
90
- * createStartHandler({
91
- * handler: defaultStreamHandler,
92
- * transformAssets: 'https://cdn.example.com',
93
- * })
94
- * ```
95
- *
96
- * **Object shorthand** — a URL prefix with optional `crossOrigin`:
97
- * ```ts
98
- * createStartHandler({
99
- * handler: defaultStreamHandler,
100
- * transformAssets: {
101
- * prefix: 'https://cdn.example.com',
102
- * crossOrigin: 'anonymous',
103
- * },
104
- * })
105
- * ```
106
- *
107
- * `crossOrigin` accepts a single value or a per-kind record:
108
- * ```ts
109
- * transformAssets: {
110
- * prefix: 'https://cdn.example.com',
111
- * crossOrigin: {
112
- * modulepreload: 'anonymous',
113
- * stylesheet: 'use-credentials',
114
- * },
115
- * }
116
- * ```
117
- *
118
- * **Callback** — receives `{ kind, url }` and returns either a string URL or
119
- * `{ href, crossOrigin? }` (cached by default — runs once on first request):
120
- * ```ts
121
- * createStartHandler({
122
- * handler: defaultStreamHandler,
123
- * transformAssets: ({ kind, url }) => {
124
- * const href = `https://cdn.example.com${url}`
125
- *
126
- * if (kind === 'modulepreload') {
127
- * return { href, crossOrigin: 'anonymous' }
128
- * }
129
- *
130
- * return { href }
131
- * },
132
- * })
133
- * ```
134
- *
135
- * **Object** — for explicit cache control:
136
- * ```ts
137
- * createStartHandler({
138
- * handler: defaultStreamHandler,
139
- * transformAssets: {
140
- * transform: ({ url }) => {
141
- * const region = getRequest().headers.get('x-region') || 'us'
142
- * return { href: `https://cdn-${region}.example.com${url}` }
143
- * },
144
- * cache: false,
145
- * },
146
- * })
147
- * ```
148
- *
149
- * `kind` is one of `'modulepreload' | 'stylesheet' | 'clientEntry'`.
150
- * `crossOrigin` applies to manifest-managed `<link>` assets.
151
- *
152
- * By default, the transformed manifest is cached after the first request
153
- * (`cache: true`). Set `cache: false` for per-request transforms.
154
- *
155
- * If you're using a cached transform, you can optionally set `warmup: true`
156
- * (object form only) to compute the transformed manifest in the background at
157
- * server startup.
158
- *
159
- * Note: This only transforms URLs managed by TanStack Start's manifest
160
- * (JS preloads, CSS links, and the client entry script). For asset imports
161
- * used directly in components (e.g. `import logo from './logo.svg'`),
162
- * configure Vite's `experimental.renderBuiltUrl` in your vite.config.ts.
163
- */
164
- transformAssets?: TransformAssets
165
- /**
166
- * @deprecated Use `transformAssets` instead.
167
- *
168
- * **String** — a URL prefix prepended to every asset URL (cached by default):
169
- * ```ts
170
- * createStartHandler({
171
- * handler: defaultStreamHandler,
172
- * transformAssetUrls: 'https://cdn.example.com',
173
- * })
174
- * ```
175
- *
176
- * **Callback** — receives `{ url, type }` and returns a new URL
177
- * (cached by default — runs once on first request):
178
- * ```ts
179
- * createStartHandler({
180
- * handler: defaultStreamHandler,
181
- * transformAssetUrls: ({ url, type }) => {
182
- * return `https://cdn.example.com${url}`
183
- * },
184
- * })
185
- * ```
186
- *
187
- * **Object** — for explicit cache control:
188
- * ```ts
189
- * createStartHandler({
190
- * handler: defaultStreamHandler,
191
- * transformAssetUrls: {
192
- * transform: ({ url }) => {
193
- * const region = getRequest().headers.get('x-region') || 'us'
194
- * return `https://cdn-${region}.example.com${url}`
195
- * },
196
- * cache: false, // transform per-request
197
- * },
198
- * })
199
- * ```
200
- *
201
- * `type` is one of `'modulepreload' | 'stylesheet' | 'clientEntry'`.
202
- *
203
- * By default, the transformed manifest is cached after the first request
204
- * (`cache: true`). Set `cache: false` for per-request transforms.
205
- *
206
- * If you're using a cached transform, you can optionally set `warmup: true`
207
- * (object form only) to compute the transformed manifest in the background at
208
- * server startup.
209
- *
210
- * Note: This only transforms URLs managed by TanStack Start's manifest
211
- * (JS preloads, CSS links, and the client entry script). For asset imports
212
- * used directly in components (e.g. `import logo from './logo.svg'`),
213
- * configure Vite's `experimental.renderBuiltUrl` in your vite.config.ts.
214
- */
215
- transformAssetUrls?: TransformAssetUrls
216
62
  }
217
63
 
218
64
  function getStartResponseHeaders(opts: { router: AnyRouter }) {
@@ -227,106 +73,6 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
227
73
  return headers
228
74
  }
229
75
 
230
- function notifyEarlyHints(
231
- phase: EarlyHintsPhase,
232
- event: EarlyHintsEvent,
233
- onEarlyHints: OnEarlyHints,
234
- ) {
235
- try {
236
- const result = onEarlyHints(event)
237
- if (result) {
238
- void Promise.resolve(result).catch((err) => {
239
- console.error(`Error sending ${phase} early hints:`, err)
240
- })
241
- }
242
- } catch (err) {
243
- console.error(`Error sending ${phase} early hints:`, err)
244
- }
245
- }
246
-
247
- function getResponseLinkHeaderFilter(
248
- responseLinkHeader: boolean | ResponseLinkHeaderOptions | undefined,
249
- ): ResponseLinkHeaderFilter | undefined {
250
- if (typeof responseLinkHeader !== 'object') {
251
- return undefined
252
- }
253
-
254
- return responseLinkHeader.filter
255
- }
256
-
257
- function appendResponseLinkHeaders(opts: {
258
- responseHeaders: Headers
259
- entries: ReadonlyArray<ResponseLinkHeaderEntry>
260
- filter?: ResponseLinkHeaderFilter
261
- }) {
262
- if (!opts.filter) {
263
- for (const entry of opts.entries) {
264
- opts.responseHeaders.append('Link', entry.link)
265
- }
266
- return
267
- }
268
-
269
- const links = getResponseLinkHeaderEntries(opts)
270
-
271
- for (const link of links) {
272
- opts.responseHeaders.append('Link', link)
273
- }
274
- }
275
-
276
- function collectResponseLinkHeaderEntries(opts: {
277
- phase: EarlyHintsPhase
278
- event: EarlyHintsEvent
279
- entries: Array<ResponseLinkHeaderEntry>
280
- }) {
281
- for (let index = 0; index < opts.event.hints.length; index++) {
282
- opts.entries.push({
283
- phase: opts.phase,
284
- hint: opts.event.hints[index]!,
285
- link: opts.event.links[index]!,
286
- })
287
- }
288
- }
289
-
290
- function handleCollectedEarlyHints(opts: {
291
- phase: EarlyHintsPhase
292
- hints: ReadonlyArray<EarlyHint>
293
- sentLinks: Set<string>
294
- sentHints?: Array<EarlyHint>
295
- onEarlyHints?: OnEarlyHints
296
- responseLinkHeaderEntries?: Array<ResponseLinkHeaderEntry>
297
- }) {
298
- const event = opts.onEarlyHints
299
- ? createEarlyHintsEvent({
300
- phase: opts.phase,
301
- hints: opts.hints,
302
- sentLinks: opts.sentLinks,
303
- sentHints: opts.sentHints!,
304
- })
305
- : undefined
306
-
307
- if (event) {
308
- notifyEarlyHints(opts.phase, event, opts.onEarlyHints!)
309
- }
310
-
311
- if (!opts.responseLinkHeaderEntries) return
312
-
313
- if (event) {
314
- collectResponseLinkHeaderEntries({
315
- phase: opts.phase,
316
- event,
317
- entries: opts.responseLinkHeaderEntries,
318
- })
319
- return
320
- }
321
-
322
- createResponseLinkHeaderEntries({
323
- phase: opts.phase,
324
- hints: opts.hints,
325
- sentLinks: opts.sentLinks,
326
- entries: opts.responseLinkHeaderEntries,
327
- })
328
- }
329
-
330
76
  interface PluginAdaptersEntry {
331
77
  hasPluginAdapters: boolean
332
78
  pluginSerializationAdapters: Array<AnySerializationAdapter>
@@ -341,17 +87,21 @@ interface Entries {
341
87
  // Cached entries - promises stored immediately to prevent concurrent imports
342
88
  // that can cause race conditions during module initialization
343
89
  let entriesPromise: Promise<Entries> | undefined
344
- let baseManifestPromise: Promise<StartManifestWithClientEntry> | undefined
345
90
  let hasWarnedMissingCsrfMiddleware = false
346
91
  const defaultCsrfMiddleware = createCsrfMiddleware({
347
92
  filter: (ctx) => ctx.handlerType === 'serverFn',
348
93
  })
349
-
350
- /**
351
- * Cached final manifest (with client entry script tag). In production,
352
- * this is computed once and reused for every request when caching is enabled.
353
- */
354
- let cachedFinalManifestPromise: Promise<Manifest> | undefined
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
355
105
 
356
106
  async function loadEntries(): Promise<Entries> {
357
107
  const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
@@ -409,61 +159,6 @@ If you intentionally handle CSRF another way, disable this warning:
409
159
  })`)
410
160
  }
411
161
 
412
- /**
413
- * Returns the raw manifest data (without client entry script tag baked in).
414
- * In dev mode, always returns fresh data. In prod, cached.
415
- */
416
- function getBaseManifest(
417
- matchedRoutes?: ReadonlyArray<AnyRoute>,
418
- ): Promise<StartManifestWithClientEntry> {
419
- // In dev mode, always get fresh manifest (no caching) to include route-specific dev styles
420
- if (process.env.TSS_DEV_SERVER === 'true') {
421
- return getStartManifest(matchedRoutes)
422
- }
423
- // In prod, cache the base manifest
424
- if (!baseManifestPromise) {
425
- baseManifestPromise = getStartManifest()
426
- }
427
- return baseManifestPromise
428
- }
429
-
430
- /**
431
- * Resolves a final Manifest for a given request.
432
- *
433
- * - No transform: builds client entry script tag and returns (cached in prod).
434
- * - Cached transform: transforms all URLs + builds script tag, caches result.
435
- * - Per-request transform: deep-clones base manifest, transforms per-request.
436
- */
437
- async function resolveManifest(
438
- matchedRoutes: ReadonlyArray<AnyRoute> | undefined,
439
- transformFn: TransformAssetsFn | undefined,
440
- cache: boolean,
441
- ): Promise<Manifest> {
442
- const base = await getBaseManifest(matchedRoutes)
443
-
444
- const computeFinalManifest = async () => {
445
- return transformFn
446
- ? await transformManifestAssets(base, transformFn, { clone: !cache })
447
- : buildManifestWithClientEntry(base)
448
- }
449
-
450
- // In dev, always compute fresh to include route-specific dev styles.
451
- if (process.env.TSS_DEV_SERVER === 'true') {
452
- return computeFinalManifest()
453
- }
454
-
455
- // In prod, cache unless we're explicitly doing per-request transforms.
456
- if (!transformFn || cache) {
457
- if (!cachedFinalManifestPromise) {
458
- cachedFinalManifestPromise = computeFinalManifest()
459
- }
460
- return cachedFinalManifestPromise
461
- }
462
-
463
- // Per-request transform — deep-clone and transform every time.
464
- return computeFinalManifest()
465
- }
466
-
467
162
  // Pre-computed constants
468
163
  const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
469
164
  const SERVER_FN_BASE = process.env.TSS_SERVER_FN_BASE
@@ -608,96 +303,22 @@ function handlerToMiddleware(
608
303
  export function createStartHandler<TRegister = Register>(
609
304
  cbOrOptions: HandlerCallback<AnyRouter> | CreateStartHandlerOptions,
610
305
  ): RequestHandler<TRegister> {
611
- // Normalize the overloaded argument
306
+ const handlerOptions: FinalManifestOptions =
307
+ typeof cbOrOptions === 'function' ? {} : cbOrOptions
612
308
  const cb: HandlerCallback<AnyRouter> =
613
309
  typeof cbOrOptions === 'function' ? cbOrOptions : cbOrOptions.handler
614
- const transformAssetsOption: TransformAssets | undefined =
615
- typeof cbOrOptions === 'function' ? undefined : cbOrOptions.transformAssets
616
- const transformAssetUrlsOption: TransformAssetUrls | undefined =
617
- typeof cbOrOptions === 'function'
618
- ? undefined
619
- : cbOrOptions.transformAssetUrls
620
-
621
- const transformOption =
622
- transformAssetsOption !== undefined
623
- ? resolveTransformAssetsConfig(transformAssetsOption)
624
- : transformAssetUrlsOption !== undefined
625
- ? resolveTransformAssetsConfig(
626
- adaptTransformAssetUrlsConfigToTransformAssets(
627
- transformAssetUrlsOption,
628
- ),
629
- )
630
- : undefined
631
-
632
- const warmupTransformManifest =
633
- (!!transformAssetsOption &&
634
- typeof transformAssetsOption === 'object' &&
635
- 'warmup' in transformAssetsOption &&
636
- transformAssetsOption.warmup === true) ||
637
- (!!transformAssetUrlsOption &&
638
- typeof transformAssetUrlsOption === 'object' &&
639
- transformAssetUrlsOption.warmup === true)
640
-
641
- // Pre-resolve the transform function and cache flag
642
- const resolvedTransformConfig = transformOption
643
- const cache = resolvedTransformConfig ? resolvedTransformConfig.cache : true
644
- const shouldCacheCreateTransform =
645
- cache && process.env.TSS_DEV_SERVER !== 'true'
646
-
647
- // Memoize a single createTransform() result when caching is enabled outside
648
- // of the dev server.
649
- let cachedCreateTransformPromise: Promise<TransformAssetsFn> | undefined
650
-
651
- const getTransformFn = async (
652
- opts: { warmup: true } | { warmup: false; request: Request },
653
- ): Promise<TransformAssetsFn | undefined> => {
654
- if (!resolvedTransformConfig) return undefined
655
-
656
- if (resolvedTransformConfig.type === 'createTransform') {
657
- if (shouldCacheCreateTransform) {
658
- if (!cachedCreateTransformPromise) {
659
- cachedCreateTransformPromise = Promise.resolve(
660
- resolvedTransformConfig.createTransform(opts),
661
- ).catch((error) => {
662
- cachedCreateTransformPromise = undefined
663
- throw error
664
- })
665
- }
666
-
667
- return cachedCreateTransformPromise
668
- }
669
-
670
- return resolvedTransformConfig.createTransform(opts)
671
- }
672
-
673
- return resolvedTransformConfig.transformFn
674
- }
675
-
676
- // Background warmup for cached transforms (production only)
677
- if (
678
- warmupTransformManifest &&
679
- cache &&
680
- process.env.TSS_DEV_SERVER !== 'true' &&
681
- !cachedFinalManifestPromise
682
- ) {
683
- // NOTE: Do not call resolveManifest() here.
684
- // resolveManifest() reads from cachedFinalManifestPromise, and since we set
685
- // cachedFinalManifestPromise to this warmup promise, that would create a
686
- // self-referential promise and hang forever.
687
- const warmupPromise = (async () => {
688
- const base = await getBaseManifest(undefined)
689
- const transformFn = await getTransformFn({ warmup: true })
690
- return transformFn
691
- ? await transformManifestAssets(base, transformFn, { clone: false })
692
- : buildManifestWithClientEntry(base)
693
- })()
694
- cachedFinalManifestPromise = warmupPromise
695
- warmupPromise.catch(() => {
696
- // If warmup fails, allow the next request to retry.
697
- if (cachedFinalManifestPromise === warmupPromise) {
698
- cachedFinalManifestPromise = undefined
699
- }
700
- 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),
701
322
  })
702
323
  }
703
324
 
@@ -856,44 +477,18 @@ export function createStartHandler<TRegister = Register>(
856
477
  )
857
478
  }
858
479
 
859
- const manifest = await resolveManifest(
860
- matchedRoutes,
861
- await getTransformFn({ warmup: false, request }),
862
- cache,
863
- )
480
+ const manifest = await resolveManifestForRequest({
481
+ request,
482
+ requestInlineCss: requestOpts?.inlineCss,
483
+ getBaseManifest: () => getBaseManifest(matchedRoutes),
484
+ })
864
485
 
865
- const onEarlyHints = requestOpts?.onEarlyHints
866
- const responseLinkHeader = requestOpts?.responseLinkHeader
867
- const shouldCollectEarlyHints =
868
- process.env.TSS_DEV_SERVER !== 'true' &&
869
- (!!onEarlyHints || !!responseLinkHeader)
870
- const sentEarlyHintLinks = shouldCollectEarlyHints
871
- ? new Set<string>()
872
- : undefined
873
- const sentEarlyHints = onEarlyHints ? new Array<EarlyHint>() : undefined
874
- const responseLinkHeaderEntries =
875
- shouldCollectEarlyHints && responseLinkHeader
876
- ? new Array<ResponseLinkHeaderEntry>()
877
- : undefined
878
- const responseLinkHeaderFilter = shouldCollectEarlyHints
879
- ? getResponseLinkHeaderFilter(responseLinkHeader)
880
- : undefined
486
+ const earlyHints = createEarlyHintsForRequest({
487
+ onEarlyHints: requestOpts?.onEarlyHints,
488
+ responseLinkHeader: requestOpts?.responseLinkHeader,
489
+ })
881
490
 
882
- if (
883
- shouldCollectEarlyHints &&
884
- sentEarlyHintLinks &&
885
- matchedRoutes?.length
886
- ) {
887
- const hints = collectStaticHintsFromManifest(manifest, matchedRoutes)
888
- handleCollectedEarlyHints({
889
- phase: 'static',
890
- hints,
891
- sentLinks: sentEarlyHintLinks,
892
- sentHints: sentEarlyHints,
893
- onEarlyHints,
894
- responseLinkHeaderEntries,
895
- })
896
- }
491
+ earlyHints?.collectStatic({ manifest, matchedRoutes })
897
492
 
898
493
  const routerInstance = await getRouter()
899
494
 
@@ -912,18 +507,7 @@ export function createStartHandler<TRegister = Register>(
912
507
  return routerInstance.state.redirect
913
508
  }
914
509
 
915
- if (shouldCollectEarlyHints && sentEarlyHintLinks) {
916
- const loadedMatches = routerInstance.stores.matches.get()
917
- const hints = collectDynamicHintsFromMatches(loadedMatches)
918
- handleCollectedEarlyHints({
919
- phase: 'dynamic',
920
- hints,
921
- sentLinks: sentEarlyHintLinks,
922
- sentHints: sentEarlyHints,
923
- onEarlyHints,
924
- responseLinkHeaderEntries,
925
- })
926
- }
510
+ earlyHints?.collectDynamic(routerInstance.stores.matches.get())
927
511
 
928
512
  // Pass request-scoped assets to dehydrate for manifest injection
929
513
  const ctx = getStartContext({ throwIfNotFound: false })
@@ -934,13 +518,7 @@ export function createStartHandler<TRegister = Register>(
934
518
  const responseHeaders = getStartResponseHeaders({
935
519
  router: routerInstance,
936
520
  })
937
- if (responseLinkHeaderEntries?.length) {
938
- appendResponseLinkHeaders({
939
- responseHeaders,
940
- entries: responseLinkHeaderEntries,
941
- filter: responseLinkHeaderFilter,
942
- })
943
- }
521
+ earlyHints?.appendResponseHeaders(responseHeaders)
944
522
  cbWillCleanup = true
945
523
 
946
524
  return cb({