@tanstack/start-server-core
Version:
Modern and scalable routing for React applications
286 lines (258 loc) • 7.63 kB
text/typescript
import { rootRouteId } from '@tanstack/router-core'
import type {
Awaitable,
Manifest,
RouterManagedTag,
} from '@tanstack/router-core'
export type AssetUrlType = 'modulepreload' | 'stylesheet' | 'clientEntry'
export interface TransformAssetUrlsContext {
url: string
type: AssetUrlType
}
export type TransformAssetUrlsFn = (
context: TransformAssetUrlsContext,
) => Awaitable<string>
export type CreateTransformAssetUrlsContext =
| {
/** True when the server is computing the cached manifest during startup warmup. */
warmup: true
}
| {
/**
* The current Request.
*
* Only available during request handling (i.e. when `warmup: false`).
*/
request: Request
/** False when transforming URLs as part of request handling. */
warmup: false
}
/**
* Async factory that runs once per manifest computation and returns the
* per-asset transform.
*/
export type CreateTransformAssetUrlsFn = (
ctx: CreateTransformAssetUrlsContext,
) => Awaitable<TransformAssetUrlsFn>
type TransformAssetUrlsOptionsBase = {
/**
* Whether to cache the transformed manifest after the first request.
*
* When `true` (default), the transform runs once on the first request and
* the resulting manifest is reused for all subsequent requests in production.
*
* Set to `false` for per-request transforms (e.g. geo-routing to different
* CDNs based on request headers).
*
* @default true
*/
cache?: boolean
/**
* When `true`, warms up the cached transformed manifest in the background when
* the server starts (production only).
*
* This can reduce latency for the first request when `cache` is `true`.
* Has no effect when `cache: false` (per-request transforms) or in dev mode.
*
* @default false
*/
warmup?: boolean
}
export type TransformAssetUrlsOptions =
| (TransformAssetUrlsOptionsBase & {
/**
* The transform to apply to asset URLs. Can be a string prefix or a callback.
*
* **String** — prepended to every asset URL.
* **Callback** — receives `{ url, type }` and returns a new URL.
*/
transform: string | TransformAssetUrlsFn
createTransform?: never
})
| (TransformAssetUrlsOptionsBase & {
/**
* Create a per-asset transform function.
*
* This factory runs once per manifest computation (per request when
* `cache: false`, or once per server when `cache: true`). It can do async
* setup work (fetch config, read from a KV, etc.) and return a fast
* per-asset transformer.
*/
createTransform: CreateTransformAssetUrlsFn
transform?: never
})
export type TransformAssetUrls =
| string
| TransformAssetUrlsFn
| TransformAssetUrlsOptions
export type ResolvedTransformAssetUrlsConfig =
| {
type: 'transform'
transformFn: TransformAssetUrlsFn
cache: boolean
}
| {
type: 'createTransform'
createTransform: CreateTransformAssetUrlsFn
cache: boolean
}
/**
* Resolves a TransformAssetUrls value (string prefix, callback, or options
* object) into a concrete transform function and cache flag.
*/
export function resolveTransformConfig(
transform: TransformAssetUrls,
): ResolvedTransformAssetUrlsConfig {
// String shorthand
if (typeof transform === 'string') {
const prefix = transform
return {
type: 'transform',
transformFn: ({ url }) => `${prefix}${url}`,
cache: true,
}
}
// Callback shorthand
if (typeof transform === 'function') {
return {
type: 'transform',
transformFn: transform,
cache: true,
}
}
// Options object
if ('createTransform' in transform && transform.createTransform) {
return {
type: 'createTransform',
createTransform: transform.createTransform,
cache: transform.cache !== false,
}
}
const transformFn =
typeof transform.transform === 'string'
? ((({ url }: TransformAssetUrlsContext) =>
`${transform.transform}${url}`) as TransformAssetUrlsFn)
: transform.transform
return {
type: 'transform',
transformFn,
cache: transform.cache !== false,
}
}
export interface StartManifestWithClientEntry {
manifest: Manifest
clientEntry: string
/** Script content prepended before the client entry import (dev only) */
injectedHeadScripts?: string
}
/**
* Builds the client entry `<script>` tag from a (possibly transformed) client
* entry URL and optional injected head scripts.
*/
export function buildClientEntryScriptTag(
clientEntry: string,
injectedHeadScripts?: string,
): RouterManagedTag {
const clientEntryLiteral = JSON.stringify(clientEntry)
let script = `import(${clientEntryLiteral})`
if (injectedHeadScripts) {
script = `${injectedHeadScripts};${script}`
}
return {
tag: 'script',
attrs: {
type: 'module',
async: true,
},
children: script,
}
}
/**
* Applies a URL transform to every asset URL in the manifest and returns a
* new manifest with a client entry script tag appended to the root route's
* assets.
*
* The source manifest is deep-cloned so the cached original is never mutated.
*/
export function transformManifestUrls(
source: StartManifestWithClientEntry,
transformFn: TransformAssetUrlsFn,
opts?: {
/** When true, clone the source manifest before mutating it. */
clone?: boolean
},
): Promise<Manifest> {
return (async () => {
const manifest = opts?.clone
? structuredClone(source.manifest)
: source.manifest
for (const route of Object.values(manifest.routes)) {
// Transform preload URLs (modulepreload)
if (route.preloads) {
route.preloads = await Promise.all(
route.preloads.map((url) =>
Promise.resolve(transformFn({ url, type: 'modulepreload' })),
),
)
}
// Transform asset tag URLs
if (route.assets) {
for (const asset of route.assets) {
if (asset.tag === 'link' && asset.attrs?.href) {
asset.attrs.href = await Promise.resolve(
transformFn({
url: asset.attrs.href,
type: 'stylesheet',
}),
)
}
}
}
}
// Transform and append the client entry script tag
const transformedClientEntry = await Promise.resolve(
transformFn({
url: source.clientEntry,
type: 'clientEntry',
}),
)
const rootRoute = manifest.routes[rootRouteId]
if (rootRoute) {
rootRoute.assets = rootRoute.assets || []
rootRoute.assets.push(
buildClientEntryScriptTag(
transformedClientEntry,
source.injectedHeadScripts,
),
)
}
return manifest
})()
}
/**
* Builds a final Manifest from a StartManifestWithClientEntry without any
* URL transforms. Used when no transformAssetUrls option is provided.
*
* Returns a new manifest object so the cached base manifest is never mutated.
*/
export function buildManifestWithClientEntry(
source: StartManifestWithClientEntry,
): Manifest {
const scriptTag = buildClientEntryScriptTag(
source.clientEntry,
source.injectedHeadScripts,
)
const baseRootRoute = source.manifest.routes[rootRouteId]
const routes = {
...source.manifest.routes,
...(baseRootRoute
? {
[rootRouteId]: {
...baseRootRoute,
assets: [...(baseRootRoute.assets || []), scriptTag],
},
}
: {}),
}
return { routes }
}