@tanstack/start-server-core 1.158.4 → 1.159.0

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.
@@ -0,0 +1,285 @@
1
+ import { rootRouteId } from '@tanstack/router-core'
2
+
3
+ import type {
4
+ Awaitable,
5
+ Manifest,
6
+ RouterManagedTag,
7
+ } from '@tanstack/router-core'
8
+
9
+ export type AssetUrlType = 'modulepreload' | 'stylesheet' | 'clientEntry'
10
+
11
+ export interface TransformAssetUrlsContext {
12
+ url: string
13
+ type: AssetUrlType
14
+ }
15
+
16
+ export type TransformAssetUrlsFn = (
17
+ context: TransformAssetUrlsContext,
18
+ ) => Awaitable<string>
19
+
20
+ export type CreateTransformAssetUrlsContext =
21
+ | {
22
+ /** True when the server is computing the cached manifest during startup warmup. */
23
+ warmup: true
24
+ }
25
+ | {
26
+ /**
27
+ * The current Request.
28
+ *
29
+ * Only available during request handling (i.e. when `warmup: false`).
30
+ */
31
+ request: Request
32
+ /** False when transforming URLs as part of request handling. */
33
+ warmup: false
34
+ }
35
+
36
+ /**
37
+ * Async factory that runs once per manifest computation and returns the
38
+ * per-asset transform.
39
+ */
40
+ export type CreateTransformAssetUrlsFn = (
41
+ ctx: CreateTransformAssetUrlsContext,
42
+ ) => Awaitable<TransformAssetUrlsFn>
43
+
44
+ type TransformAssetUrlsOptionsBase = {
45
+ /**
46
+ * Whether to cache the transformed manifest after the first request.
47
+ *
48
+ * When `true` (default), the transform runs once on the first request and
49
+ * the resulting manifest is reused for all subsequent requests in production.
50
+ *
51
+ * Set to `false` for per-request transforms (e.g. geo-routing to different
52
+ * CDNs based on request headers).
53
+ *
54
+ * @default true
55
+ */
56
+ cache?: boolean
57
+
58
+ /**
59
+ * When `true`, warms up the cached transformed manifest in the background when
60
+ * the server starts (production only).
61
+ *
62
+ * This can reduce latency for the first request when `cache` is `true`.
63
+ * Has no effect when `cache: false` (per-request transforms) or in dev mode.
64
+ *
65
+ * @default false
66
+ */
67
+ warmup?: boolean
68
+ }
69
+
70
+ export type TransformAssetUrlsOptions =
71
+ | (TransformAssetUrlsOptionsBase & {
72
+ /**
73
+ * The transform to apply to asset URLs. Can be a string prefix or a callback.
74
+ *
75
+ * **String** — prepended to every asset URL.
76
+ * **Callback** — receives `{ url, type }` and returns a new URL.
77
+ */
78
+ transform: string | TransformAssetUrlsFn
79
+ createTransform?: never
80
+ })
81
+ | (TransformAssetUrlsOptionsBase & {
82
+ /**
83
+ * Create a per-asset transform function.
84
+ *
85
+ * This factory runs once per manifest computation (per request when
86
+ * `cache: false`, or once per server when `cache: true`). It can do async
87
+ * setup work (fetch config, read from a KV, etc.) and return a fast
88
+ * per-asset transformer.
89
+ */
90
+ createTransform: CreateTransformAssetUrlsFn
91
+ transform?: never
92
+ })
93
+
94
+ export type TransformAssetUrls =
95
+ | string
96
+ | TransformAssetUrlsFn
97
+ | TransformAssetUrlsOptions
98
+
99
+ export type ResolvedTransformAssetUrlsConfig =
100
+ | {
101
+ type: 'transform'
102
+ transformFn: TransformAssetUrlsFn
103
+ cache: boolean
104
+ }
105
+ | {
106
+ type: 'createTransform'
107
+ createTransform: CreateTransformAssetUrlsFn
108
+ cache: boolean
109
+ }
110
+
111
+ /**
112
+ * Resolves a TransformAssetUrls value (string prefix, callback, or options
113
+ * object) into a concrete transform function and cache flag.
114
+ */
115
+ export function resolveTransformConfig(
116
+ transform: TransformAssetUrls,
117
+ ): ResolvedTransformAssetUrlsConfig {
118
+ // String shorthand
119
+ if (typeof transform === 'string') {
120
+ const prefix = transform
121
+ return {
122
+ type: 'transform',
123
+ transformFn: ({ url }) => `${prefix}${url}`,
124
+ cache: true,
125
+ }
126
+ }
127
+
128
+ // Callback shorthand
129
+ if (typeof transform === 'function') {
130
+ return {
131
+ type: 'transform',
132
+ transformFn: transform,
133
+ cache: true,
134
+ }
135
+ }
136
+
137
+ // Options object
138
+ if ('createTransform' in transform && transform.createTransform) {
139
+ return {
140
+ type: 'createTransform',
141
+ createTransform: transform.createTransform,
142
+ cache: transform.cache !== false,
143
+ }
144
+ }
145
+
146
+ const transformFn =
147
+ typeof transform.transform === 'string'
148
+ ? ((({ url }: TransformAssetUrlsContext) =>
149
+ `${transform.transform}${url}`) as TransformAssetUrlsFn)
150
+ : transform.transform
151
+
152
+ return {
153
+ type: 'transform',
154
+ transformFn,
155
+ cache: transform.cache !== false,
156
+ }
157
+ }
158
+
159
+ export interface StartManifestWithClientEntry {
160
+ manifest: Manifest
161
+ clientEntry: string
162
+ /** Script content prepended before the client entry import (dev only) */
163
+ injectedHeadScripts?: string
164
+ }
165
+
166
+ /**
167
+ * Builds the client entry `<script>` tag from a (possibly transformed) client
168
+ * entry URL and optional injected head scripts.
169
+ */
170
+ export function buildClientEntryScriptTag(
171
+ clientEntry: string,
172
+ injectedHeadScripts?: string,
173
+ ): RouterManagedTag {
174
+ const clientEntryLiteral = JSON.stringify(clientEntry)
175
+ let script = `import(${clientEntryLiteral})`
176
+ if (injectedHeadScripts) {
177
+ script = `${injectedHeadScripts};${script}`
178
+ }
179
+ return {
180
+ tag: 'script',
181
+ attrs: {
182
+ type: 'module',
183
+ async: true,
184
+ },
185
+ children: script,
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Applies a URL transform to every asset URL in the manifest and returns a
191
+ * new manifest with a client entry script tag appended to the root route's
192
+ * assets.
193
+ *
194
+ * The source manifest is deep-cloned so the cached original is never mutated.
195
+ */
196
+ export function transformManifestUrls(
197
+ source: StartManifestWithClientEntry,
198
+ transformFn: TransformAssetUrlsFn,
199
+ opts?: {
200
+ /** When true, clone the source manifest before mutating it. */
201
+ clone?: boolean
202
+ },
203
+ ): Promise<Manifest> {
204
+ return (async () => {
205
+ const manifest = opts?.clone
206
+ ? structuredClone(source.manifest)
207
+ : source.manifest
208
+
209
+ for (const route of Object.values(manifest.routes)) {
210
+ // Transform preload URLs (modulepreload)
211
+ if (route.preloads) {
212
+ route.preloads = await Promise.all(
213
+ route.preloads.map((url) =>
214
+ Promise.resolve(transformFn({ url, type: 'modulepreload' })),
215
+ ),
216
+ )
217
+ }
218
+
219
+ // Transform asset tag URLs
220
+ if (route.assets) {
221
+ for (const asset of route.assets) {
222
+ if (asset.tag === 'link' && asset.attrs?.href) {
223
+ asset.attrs.href = await Promise.resolve(
224
+ transformFn({
225
+ url: asset.attrs.href,
226
+ type: 'stylesheet',
227
+ }),
228
+ )
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ // Transform and append the client entry script tag
235
+ const transformedClientEntry = await Promise.resolve(
236
+ transformFn({
237
+ url: source.clientEntry,
238
+ type: 'clientEntry',
239
+ }),
240
+ )
241
+
242
+ const rootRoute = manifest.routes[rootRouteId]
243
+ if (rootRoute) {
244
+ rootRoute.assets = rootRoute.assets || []
245
+ rootRoute.assets.push(
246
+ buildClientEntryScriptTag(
247
+ transformedClientEntry,
248
+ source.injectedHeadScripts,
249
+ ),
250
+ )
251
+ }
252
+
253
+ return manifest
254
+ })()
255
+ }
256
+
257
+ /**
258
+ * Builds a final Manifest from a StartManifestWithClientEntry without any
259
+ * URL transforms. Used when no transformAssetUrls option is provided.
260
+ *
261
+ * Returns a new manifest object so the cached base manifest is never mutated.
262
+ */
263
+ export function buildManifestWithClientEntry(
264
+ source: StartManifestWithClientEntry,
265
+ ): Manifest {
266
+ const scriptTag = buildClientEntryScriptTag(
267
+ source.clientEntry,
268
+ source.injectedHeadScripts,
269
+ )
270
+
271
+ const baseRootRoute = source.manifest.routes[rootRouteId]
272
+ const routes = {
273
+ ...source.manifest.routes,
274
+ ...(baseRootRoute
275
+ ? {
276
+ [rootRouteId]: {
277
+ ...baseRootRoute,
278
+ assets: [...(baseRootRoute.assets || []), scriptTag],
279
+ },
280
+ }
281
+ : {}),
282
+ }
283
+
284
+ return { routes }
285
+ }