@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.
@@ -47,6 +47,15 @@ export type ResponseLinkHeaderOptions = {
47
47
  filter?: ResponseLinkHeaderFilter
48
48
  }
49
49
 
50
+ export interface EarlyHintsCollector {
51
+ collectStatic: (opts: {
52
+ manifest: Manifest
53
+ matchedRoutes?: ReadonlyArray<AnyRoute>
54
+ }) => void
55
+ collectDynamic: (matches: ReadonlyArray<AnyRouteMatch>) => void
56
+ appendResponseHeaders: (headers: Headers) => void
57
+ }
58
+
50
59
  const LINK_PARAM_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
51
60
  const PRELOAD_AS_VALUES = new Set<EarlyHint['as']>([
52
61
  'fetch',
@@ -293,3 +302,153 @@ export function getResponseLinkHeaderEntries(opts: {
293
302
  return []
294
303
  }
295
304
  }
305
+
306
+ function notifyEarlyHints(
307
+ phase: EarlyHintsPhase,
308
+ event: EarlyHintsEvent,
309
+ onEarlyHints: OnEarlyHints,
310
+ ) {
311
+ try {
312
+ const result = onEarlyHints(event)
313
+ if (result) {
314
+ void Promise.resolve(result).catch((err) => {
315
+ console.error(`Error sending ${phase} early hints:`, err)
316
+ })
317
+ }
318
+ } catch (err) {
319
+ console.error(`Error sending ${phase} early hints:`, err)
320
+ }
321
+ }
322
+
323
+ function getResponseLinkHeaderFilter(
324
+ responseLinkHeader: boolean | ResponseLinkHeaderOptions | undefined,
325
+ ): ResponseLinkHeaderFilter | undefined {
326
+ if (typeof responseLinkHeader !== 'object') {
327
+ return undefined
328
+ }
329
+
330
+ return responseLinkHeader.filter
331
+ }
332
+
333
+ function appendResponseLinkHeaders(opts: {
334
+ responseHeaders: Headers
335
+ entries: ReadonlyArray<ResponseLinkHeaderEntry>
336
+ filter?: ResponseLinkHeaderFilter
337
+ }) {
338
+ for (const link of getResponseLinkHeaderEntries(opts)) {
339
+ opts.responseHeaders.append('Link', link)
340
+ }
341
+ }
342
+
343
+ function collectResponseLinkHeaderEntries(opts: {
344
+ phase: EarlyHintsPhase
345
+ event: EarlyHintsEvent
346
+ entries: Array<ResponseLinkHeaderEntry>
347
+ }) {
348
+ for (let index = 0; index < opts.event.hints.length; index++) {
349
+ opts.entries.push({
350
+ phase: opts.phase,
351
+ hint: opts.event.hints[index]!,
352
+ link: opts.event.links[index]!,
353
+ })
354
+ }
355
+ }
356
+
357
+ function collectEarlyHintsPhase(opts: {
358
+ phase: EarlyHintsPhase
359
+ hints: ReadonlyArray<EarlyHint>
360
+ sentLinks: Set<string>
361
+ sentHints?: Array<EarlyHint>
362
+ onEarlyHints?: OnEarlyHints
363
+ responseLinkHeaderEntries?: Array<ResponseLinkHeaderEntry>
364
+ }) {
365
+ const event = opts.onEarlyHints
366
+ ? createEarlyHintsEvent({
367
+ phase: opts.phase,
368
+ hints: opts.hints,
369
+ sentLinks: opts.sentLinks,
370
+ sentHints: opts.sentHints!,
371
+ })
372
+ : undefined
373
+
374
+ if (event) {
375
+ notifyEarlyHints(opts.phase, event, opts.onEarlyHints!)
376
+ }
377
+
378
+ if (!opts.responseLinkHeaderEntries) return
379
+
380
+ if (event) {
381
+ collectResponseLinkHeaderEntries({
382
+ phase: opts.phase,
383
+ event,
384
+ entries: opts.responseLinkHeaderEntries,
385
+ })
386
+ return
387
+ }
388
+
389
+ createResponseLinkHeaderEntries({
390
+ phase: opts.phase,
391
+ hints: opts.hints,
392
+ sentLinks: opts.sentLinks,
393
+ entries: opts.responseLinkHeaderEntries,
394
+ })
395
+ }
396
+
397
+ export function createEarlyHintsCollector(
398
+ opts:
399
+ | {
400
+ onEarlyHints?: OnEarlyHints
401
+ responseLinkHeader?: boolean | ResponseLinkHeaderOptions
402
+ }
403
+ | undefined,
404
+ ): EarlyHintsCollector | undefined {
405
+ if (
406
+ process.env.TSS_DEV_SERVER === 'true' ||
407
+ (!opts?.onEarlyHints && !opts?.responseLinkHeader)
408
+ ) {
409
+ return undefined
410
+ }
411
+
412
+ const sentLinks = new Set<string>()
413
+ const sentHints = opts.onEarlyHints ? new Array<EarlyHint>() : undefined
414
+ const responseLinkHeaderEntries = opts.responseLinkHeader
415
+ ? new Array<ResponseLinkHeaderEntry>()
416
+ : undefined
417
+ const responseLinkHeaderFilter = getResponseLinkHeaderFilter(
418
+ opts.responseLinkHeader,
419
+ )
420
+
421
+ return {
422
+ collectStatic: ({ manifest, matchedRoutes }) => {
423
+ if (!matchedRoutes?.length) return
424
+
425
+ collectEarlyHintsPhase({
426
+ phase: 'static',
427
+ hints: collectStaticHintsFromManifest(manifest, matchedRoutes),
428
+ sentLinks,
429
+ sentHints,
430
+ onEarlyHints: opts.onEarlyHints,
431
+ responseLinkHeaderEntries,
432
+ })
433
+ },
434
+ collectDynamic: (matches) => {
435
+ collectEarlyHintsPhase({
436
+ phase: 'dynamic',
437
+ hints: collectDynamicHintsFromMatches(matches),
438
+ sentLinks,
439
+ sentHints,
440
+ onEarlyHints: opts.onEarlyHints,
441
+ responseLinkHeaderEntries,
442
+ })
443
+ },
444
+ appendResponseHeaders: (headers) => {
445
+ if (!responseLinkHeaderEntries?.length) return
446
+
447
+ appendResponseLinkHeaders({
448
+ responseHeaders: headers,
449
+ entries: responseLinkHeaderEntries,
450
+ filter: responseLinkHeaderFilter,
451
+ })
452
+ },
453
+ }
454
+ }
@@ -0,0 +1,319 @@
1
+ import {
2
+ buildManifestWithClientEntry,
3
+ resolveTransformAssetsConfig,
4
+ transformManifestAssets,
5
+ } from './transformAssetUrls'
6
+ import {
7
+ getStaticHandlerInlineCssDefault,
8
+ resolveInlineCssForRequest,
9
+ } from './inlineCss'
10
+ import type { Manifest } from '@tanstack/router-core'
11
+ import type { HandlerInlineCssOption } from './inlineCss'
12
+ import type {
13
+ CreateTransformAssetsContext,
14
+ StartManifestWithClientEntry,
15
+ TransformAssets,
16
+ TransformAssetsFn,
17
+ } from './transformAssetUrls'
18
+
19
+ export type {
20
+ HandlerInlineCssOption,
21
+ StartManifestWithClientEntry,
22
+ TransformAssets,
23
+ }
24
+
25
+ export interface FinalManifestOptions {
26
+ /**
27
+ * Controls whether Start inlines build-collected CSS by default at runtime.
28
+ *
29
+ * This only has an effect when the build was created with
30
+ * `server.build.inlineCss` enabled. Pass a callback to decide per request.
31
+ * `handler(request, { inlineCss })` overrides this value for that request.
32
+ *
33
+ * @default true
34
+ */
35
+ inlineCss?: HandlerInlineCssOption
36
+ /**
37
+ * Transform manifest-managed asset URLs and attributes at runtime, e.g. to
38
+ * prepend a CDN prefix.
39
+ *
40
+ * This covers JS preloads, CSS links, the client entry script, and URLs
41
+ * inside build-collected inline CSS. Asset imports used directly in
42
+ * components should be handled by the bundler instead.
43
+ */
44
+ transformAssets?: TransformAssets
45
+ }
46
+
47
+ type FinalManifestCacheKey = 'inline-css' | 'linked-css'
48
+ type FinalManifestCache = Map<FinalManifestCacheKey, Promise<Manifest>>
49
+ export type GetBaseManifest = () => Promise<StartManifestWithClientEntry>
50
+
51
+ export interface FinalManifestRequestOptions {
52
+ request: Request
53
+ requestInlineCss: boolean | undefined
54
+ getBaseManifest: GetBaseManifest
55
+ }
56
+
57
+ interface FinalManifestTransformResolver {
58
+ cache: boolean
59
+ warmup: boolean
60
+ getTransformFn: (
61
+ ctx: CreateTransformAssetsContext,
62
+ ) => Promise<TransformAssetsFn | undefined>
63
+ clearCachedCreateTransform: () => void
64
+ }
65
+
66
+ export interface FinalManifestResolver {
67
+ warmup: (opts: {
68
+ getBaseManifest: GetBaseManifest
69
+ }) => Promise<Manifest> | undefined
70
+ resolveCached: (opts: FinalManifestRequestOptions) => Promise<Manifest>
71
+ resolveUncached: (opts: FinalManifestRequestOptions) => Promise<Manifest>
72
+ }
73
+
74
+ export function createCachedBaseManifestLoader(
75
+ loadBaseManifest: GetBaseManifest,
76
+ ): GetBaseManifest {
77
+ let baseManifestPromise: Promise<StartManifestWithClientEntry> | undefined
78
+
79
+ return () => {
80
+ if (!baseManifestPromise) {
81
+ baseManifestPromise = loadBaseManifest().catch((error) => {
82
+ baseManifestPromise = undefined
83
+ throw error
84
+ })
85
+ }
86
+
87
+ return baseManifestPromise
88
+ }
89
+ }
90
+
91
+ function createFinalManifestTransformResolver(
92
+ transformAssets: TransformAssets | undefined,
93
+ opts: { cacheCreateTransform: boolean },
94
+ ): FinalManifestTransformResolver {
95
+ const transformConfig =
96
+ transformAssets !== undefined
97
+ ? resolveTransformAssetsConfig(transformAssets)
98
+ : undefined
99
+ const cache = transformConfig ? transformConfig.cache : true
100
+ const warmup =
101
+ !!transformAssets &&
102
+ typeof transformAssets === 'object' &&
103
+ 'warmup' in transformAssets &&
104
+ transformAssets.warmup === true
105
+
106
+ let cachedCreateTransformPromise: Promise<TransformAssetsFn> | undefined
107
+
108
+ const clearCachedCreateTransform = () => {
109
+ cachedCreateTransformPromise = undefined
110
+ }
111
+
112
+ return {
113
+ cache,
114
+ warmup,
115
+ clearCachedCreateTransform,
116
+ getTransformFn: async (ctx) => {
117
+ if (!transformConfig) return undefined
118
+
119
+ if (transformConfig.type !== 'createTransform') {
120
+ return transformConfig.transformFn
121
+ }
122
+
123
+ if (!cache || !opts.cacheCreateTransform) {
124
+ return transformConfig.createTransform(ctx)
125
+ }
126
+
127
+ if (!cachedCreateTransformPromise) {
128
+ cachedCreateTransformPromise = Promise.resolve(
129
+ transformConfig.createTransform(ctx),
130
+ ).catch((error) => {
131
+ clearCachedCreateTransform()
132
+ throw error
133
+ })
134
+ }
135
+
136
+ return cachedCreateTransformPromise
137
+ },
138
+ }
139
+ }
140
+
141
+ export function createFinalManifestResolver(
142
+ opts: FinalManifestOptions & { cacheCreateTransform: boolean },
143
+ ): FinalManifestResolver {
144
+ const finalManifestCache: FinalManifestCache = new Map()
145
+ const transformResolver = createFinalManifestTransformResolver(
146
+ opts.transformAssets,
147
+ { cacheCreateTransform: opts.cacheCreateTransform },
148
+ )
149
+ const handlerDefaultInlineCss = getStaticHandlerInlineCssDefault(
150
+ opts.inlineCss,
151
+ )
152
+
153
+ const getRequestManifestOptions = async (
154
+ requestOpts: FinalManifestRequestOptions,
155
+ ) => {
156
+ const transformFn = await transformResolver.getTransformFn({
157
+ warmup: false,
158
+ request: requestOpts.request,
159
+ })
160
+ const inlineCss = await resolveInlineCssForRequest({
161
+ request: requestOpts.request,
162
+ handlerInlineCss: opts.inlineCss,
163
+ requestInlineCss: requestOpts.requestInlineCss,
164
+ })
165
+
166
+ return {
167
+ getBaseManifest: requestOpts.getBaseManifest,
168
+ transformFn,
169
+ cache: transformResolver.cache,
170
+ inlineCss,
171
+ }
172
+ }
173
+
174
+ const resolveRequest = async (
175
+ requestOpts: FinalManifestRequestOptions,
176
+ cache: FinalManifestCache | undefined,
177
+ ) => {
178
+ return resolveFinalManifest({
179
+ ...(await getRequestManifestOptions(requestOpts)),
180
+ finalManifestCache: cache,
181
+ })
182
+ }
183
+
184
+ return {
185
+ warmup: ({ getBaseManifest }) =>
186
+ warmupFinalManifest({
187
+ enabled: transformResolver.warmup,
188
+ handlerDefaultInlineCss,
189
+ cache: transformResolver.cache,
190
+ finalManifestCache,
191
+ getBaseManifest,
192
+ getTransformFn: () =>
193
+ transformResolver.getTransformFn({ warmup: true }),
194
+ onError: transformResolver.clearCachedCreateTransform,
195
+ }),
196
+ resolveCached: (requestOpts) =>
197
+ resolveRequest(requestOpts, finalManifestCache),
198
+ resolveUncached: (requestOpts) => resolveRequest(requestOpts, undefined),
199
+ }
200
+ }
201
+
202
+ function getFinalManifestCacheKey(inlineCss: boolean): FinalManifestCacheKey {
203
+ return inlineCss ? 'inline-css' : 'linked-css'
204
+ }
205
+
206
+ function cacheFinalManifestPromise(
207
+ cachedFinalManifestPromises: FinalManifestCache,
208
+ cacheKey: FinalManifestCacheKey,
209
+ promise: Promise<Manifest>,
210
+ ): Promise<Manifest> {
211
+ const cachedFinalManifestPromise = promise.catch((error) => {
212
+ if (
213
+ cachedFinalManifestPromises.get(cacheKey) === cachedFinalManifestPromise
214
+ ) {
215
+ cachedFinalManifestPromises.delete(cacheKey)
216
+ }
217
+ throw error
218
+ })
219
+
220
+ cachedFinalManifestPromises.set(cacheKey, cachedFinalManifestPromise)
221
+ return cachedFinalManifestPromise
222
+ }
223
+
224
+ function getOrCreateCachedFinalManifestPromise(
225
+ cachedFinalManifestPromises: FinalManifestCache,
226
+ cacheKey: FinalManifestCacheKey,
227
+ computeFinalManifest: () => Promise<Manifest>,
228
+ ): Promise<Manifest> {
229
+ const cachedFinalManifestPromise = cachedFinalManifestPromises.get(cacheKey)
230
+ if (cachedFinalManifestPromise) {
231
+ return cachedFinalManifestPromise
232
+ }
233
+
234
+ return cacheFinalManifestPromise(
235
+ cachedFinalManifestPromises,
236
+ cacheKey,
237
+ Promise.resolve().then(computeFinalManifest),
238
+ )
239
+ }
240
+
241
+ async function buildFinalManifest(opts: {
242
+ base: StartManifestWithClientEntry
243
+ transformFn: TransformAssetsFn | undefined
244
+ inlineCss: boolean
245
+ }): Promise<Manifest> {
246
+ return opts.transformFn
247
+ ? await transformManifestAssets(opts.base, opts.transformFn, {
248
+ inlineCss: opts.inlineCss,
249
+ })
250
+ : buildManifestWithClientEntry(opts.base, { inlineCss: opts.inlineCss })
251
+ }
252
+
253
+ async function resolveFinalManifest(opts: {
254
+ getBaseManifest: () => Promise<StartManifestWithClientEntry>
255
+ transformFn: TransformAssetsFn | undefined
256
+ cache: boolean
257
+ inlineCss: boolean
258
+ finalManifestCache?: FinalManifestCache
259
+ }): Promise<Manifest> {
260
+ const computeFinalManifest = async () => {
261
+ return buildFinalManifest({
262
+ base: await opts.getBaseManifest(),
263
+ transformFn: opts.transformFn,
264
+ inlineCss: opts.inlineCss,
265
+ })
266
+ }
267
+
268
+ if (opts.finalManifestCache && (!opts.transformFn || opts.cache)) {
269
+ return getOrCreateCachedFinalManifestPromise(
270
+ opts.finalManifestCache,
271
+ getFinalManifestCacheKey(opts.inlineCss),
272
+ computeFinalManifest,
273
+ )
274
+ }
275
+
276
+ return computeFinalManifest()
277
+ }
278
+
279
+ function warmupFinalManifest(opts: {
280
+ enabled: boolean
281
+ handlerDefaultInlineCss: boolean | undefined
282
+ cache: boolean
283
+ finalManifestCache: FinalManifestCache
284
+ getBaseManifest: () => Promise<StartManifestWithClientEntry>
285
+ getTransformFn: () => Promise<TransformAssetsFn | undefined>
286
+ onError?: () => void
287
+ }): Promise<Manifest> | undefined {
288
+ if (
289
+ !opts.enabled ||
290
+ opts.handlerDefaultInlineCss === undefined ||
291
+ !opts.cache
292
+ ) {
293
+ return undefined
294
+ }
295
+
296
+ const inlineCss = opts.handlerDefaultInlineCss
297
+ const warmupPromise = getOrCreateCachedFinalManifestPromise(
298
+ opts.finalManifestCache,
299
+ getFinalManifestCacheKey(inlineCss),
300
+ async () => {
301
+ const [base, transformFn] = await Promise.all([
302
+ opts.getBaseManifest(),
303
+ opts.getTransformFn(),
304
+ ])
305
+
306
+ return buildFinalManifest({
307
+ base,
308
+ transformFn,
309
+ inlineCss,
310
+ })
311
+ },
312
+ )
313
+
314
+ if (opts.onError) {
315
+ void warmupPromise.catch(opts.onError)
316
+ }
317
+
318
+ return warmupPromise
319
+ }
package/src/global.d.ts CHANGED
@@ -7,6 +7,7 @@ declare global {
7
7
  TSS_SHELL?: 'true' | 'false'
8
8
  TSS_PRERENDERING?: 'true' | 'false'
9
9
  TSS_DEV_SERVER?: 'true' | 'false'
10
+ TSS_DISABLE_CSRF_MIDDLEWARE_WARNING?: 'true' | 'false'
10
11
  }
11
12
  }
12
13
  }
package/src/index.tsx CHANGED
@@ -9,12 +9,8 @@ export type {
9
9
  TransformAssetsObjectShorthand,
10
10
  TransformAssetsCrossOriginConfig,
11
11
  TransformAssetResult,
12
- TransformAssetUrls,
13
- TransformAssetUrlsFn,
14
- TransformAssetUrlsContext,
15
- TransformAssetUrlsOptions,
16
- AssetUrlType,
17
12
  TransformAssetKind,
13
+ CreateTransformAssetsContext,
18
14
  } from './transformAssetUrls'
19
15
 
20
16
  export {
@@ -0,0 +1,31 @@
1
+ import type { Awaitable } from '@tanstack/router-core'
2
+
3
+ export type HandlerInlineCssOption =
4
+ | boolean
5
+ | ((ctx: { request: Request }) => Awaitable<boolean>)
6
+
7
+ export function getStaticHandlerInlineCssDefault(
8
+ handlerInlineCss: HandlerInlineCssOption | undefined,
9
+ ) {
10
+ if (typeof handlerInlineCss === 'function') {
11
+ return undefined
12
+ }
13
+
14
+ return handlerInlineCss ?? true
15
+ }
16
+
17
+ export async function resolveInlineCssForRequest(opts: {
18
+ request: Request
19
+ handlerInlineCss: HandlerInlineCssOption | undefined
20
+ requestInlineCss: boolean | undefined
21
+ }) {
22
+ if (opts.requestInlineCss !== undefined) {
23
+ return opts.requestInlineCss
24
+ }
25
+
26
+ if (typeof opts.handlerInlineCss === 'function') {
27
+ return await opts.handlerInlineCss({ request: opts.request })
28
+ }
29
+
30
+ return opts.handlerInlineCss ?? true
31
+ }
@@ -45,7 +45,19 @@ type EarlyHintsOptions = {
45
45
  responseLinkHeader?: boolean | ResponseLinkHeaderOptions
46
46
  }
47
47
 
48
+ type InlineCssOptions = {
49
+ /**
50
+ * Controls whether Start inlines build-collected CSS for this request.
51
+ *
52
+ * This only has an effect when the build was created with
53
+ * `server.build.inlineCss` enabled. Defaults to `true` so builds with inline
54
+ * CSS enabled continue to inline CSS unless a request opts out.
55
+ */
56
+ inlineCss?: boolean
57
+ }
58
+
48
59
  export type RequestOptions<TRegister> = EarlyHintsOptions &
60
+ InlineCssOptions &
49
61
  (TRegister extends {
50
62
  server: { requestContext: infer TRequestContext }
51
63
  }