Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/router/guide/document-head-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ The `<HeadContent />` component is **required** to render the head, title, meta,

It should be **rendered either in the `<head>` tag of your root layout or as high up in the component tree as possible** if your application doesn't or can't manage the `<head>` tag.

For manifest-managed assets, you can also set `crossorigin` values on emitted
`modulepreload` and stylesheet links:

```tsx
<HeadContent assetCrossOrigin="anonymous" />

<HeadContent
assetCrossOrigin={{
modulepreload: 'anonymous',
stylesheet: 'use-credentials',
}}
/>
```

`assetCrossOrigin` only applies to manifest-managed asset links emitted by Start.
If you also set `crossOrigin` via `transformAssets` (either the object shorthand
or a callback return value), `assetCrossOrigin` wins.

### Start/Full-Stack Applications

<!-- ::start:framework -->
Expand Down
168 changes: 128 additions & 40 deletions docs/start/framework/react/guide/cdn-asset-urls.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions e2e/react-start/basic-rsc/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ declare module '@tanstack/react-router' {
'/_layout': {
id: '/_layout'
path: ''
fullPath: ''
fullPath: '/'
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
Expand Down Expand Up @@ -170,7 +170,7 @@ declare module '@tanstack/react-router' {
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
fullPath: ''
fullPath: '/'
preLoaderRoute: typeof LayoutLayout2RouteImport
parentRoute: typeof LayoutRoute
}
Expand Down Expand Up @@ -249,6 +249,7 @@ import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
2 changes: 1 addition & 1 deletion e2e/react-start/clerk-basic/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ declare module '@tanstack/react-router' {
'/_authed': {
id: '/_authed'
path: ''
fullPath: ''
fullPath: '/'
preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
Expand Down
40 changes: 21 additions & 19 deletions e2e/react-start/transform-asset-urls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,28 @@
"manual:cdn": "CDN_PORT=3002 node tests/cdn-server.mjs",
"manual:app": "pnpm build && PORT=3000 CDN_ORIGIN=http://localhost:3002 pnpm start",
"manual:both": "sh -c \"CDN_PORT=3002 node tests/cdn-server.mjs & pnpm build && PORT=3000 CDN_ORIGIN=http://localhost:3002 pnpm start\"",
"manual:both:string": "TRANSFORM_ASSET_URLS_MODE=string pnpm manual:both",
"manual:both:function": "TRANSFORM_ASSET_URLS_MODE=function pnpm manual:both",
"manual:both:options": "TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform pnpm manual:both",
"manual:both:options:transform": "TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform pnpm manual:both",
"manual:both:options:createTransform": "TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform pnpm manual:both",
"test:e2e:string": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=string playwright test --project=chromium",
"test:e2e:function": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=function playwright test --project=chromium",
"test:e2e:options": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform playwright test --project=chromium",
"test:e2e:options:transform": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform playwright test --project=chromium",
"test:e2e:options:createTransform": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform playwright test --project=chromium",
"test:e2e:options:transform:cache-true:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=true TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:transform:cache-true:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=true TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:transform:cache-false:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=false TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:transform:cache-false:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=transform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=false TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:createTransform:cache-true:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=true TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:createTransform:cache-true:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=true TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:createTransform:cache-false:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=false TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:createTransform:cache-false:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSET_URLS_MODE=options TRANSFORM_ASSET_URLS_OPTIONS_KIND=createTransform TRANSFORM_ASSET_URLS_OPTIONS_CACHE=false TRANSFORM_ASSET_URLS_OPTIONS_WARMUP=false playwright test --project=chromium",
"manual:both:string": "TRANSFORM_ASSETS_MODE=string pnpm manual:both",
"manual:both:function": "TRANSFORM_ASSETS_MODE=function pnpm manual:both",
"manual:both:options": "TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform pnpm manual:both",
"manual:both:options:transform": "TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform pnpm manual:both",
"manual:both:options:createTransform": "TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform pnpm manual:both",
"manual:both:deprecated": "USE_DEPRECATED_TRANSFORM_ASSET_URLS=true TRANSFORM_ASSETS_MODE=function pnpm manual:both",
"test:e2e:string": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=string playwright test --project=chromium",
"test:e2e:function": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=function playwright test --project=chromium",
"test:e2e:deprecated": "rm -rf dist; rm -rf port*.txt; USE_DEPRECATED_TRANSFORM_ASSET_URLS=true TRANSFORM_ASSETS_MODE=function playwright test --project=chromium",
"test:e2e:options": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform playwright test --project=chromium",
"test:e2e:options:transform": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform playwright test --project=chromium",
"test:e2e:options:createTransform": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform playwright test --project=chromium",
"test:e2e:options:transform:cache-true:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform TRANSFORM_ASSETS_OPTIONS_CACHE=true TRANSFORM_ASSETS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:transform:cache-true:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform TRANSFORM_ASSETS_OPTIONS_CACHE=true TRANSFORM_ASSETS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:transform:cache-false:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform TRANSFORM_ASSETS_OPTIONS_CACHE=false TRANSFORM_ASSETS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:transform:cache-false:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=transform TRANSFORM_ASSETS_OPTIONS_CACHE=false TRANSFORM_ASSETS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:createTransform:cache-true:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform TRANSFORM_ASSETS_OPTIONS_CACHE=true TRANSFORM_ASSETS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:createTransform:cache-true:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform TRANSFORM_ASSETS_OPTIONS_CACHE=true TRANSFORM_ASSETS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:createTransform:cache-false:warmup-true": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform TRANSFORM_ASSETS_OPTIONS_CACHE=false TRANSFORM_ASSETS_OPTIONS_WARMUP=true playwright test --project=chromium",
"test:e2e:options:createTransform:cache-false:warmup-false": "rm -rf dist; rm -rf port*.txt; TRANSFORM_ASSETS_MODE=options TRANSFORM_ASSETS_OPTIONS_KIND=createTransform TRANSFORM_ASSETS_OPTIONS_CACHE=false TRANSFORM_ASSETS_OPTIONS_WARMUP=false playwright test --project=chromium",
"test:e2e:options:matrix": "pnpm test:e2e:options:transform:cache-true:warmup-true && pnpm test:e2e:options:transform:cache-true:warmup-false && pnpm test:e2e:options:transform:cache-false:warmup-true && pnpm test:e2e:options:transform:cache-false:warmup-false && pnpm test:e2e:options:createTransform:cache-true:warmup-true && pnpm test:e2e:options:createTransform:cache-true:warmup-false && pnpm test:e2e:options:createTransform:cache-false:warmup-true && pnpm test:e2e:options:createTransform:cache-false:warmup-false",
"test:e2e": "pnpm test:e2e:string && pnpm test:e2e:function && pnpm test:e2e:options:matrix"
"test:e2e": "pnpm test:e2e:string && pnpm test:e2e:function && pnpm test:e2e:deprecated && pnpm test:e2e:options:matrix"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
Expand Down
21 changes: 12 additions & 9 deletions e2e/react-start/transform-asset-urls/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ const CDN_PORT = await getTestServerPort(`${packageJson.name}_cdn`)

const baseURL = `http://localhost:${APP_PORT}`
const cdnOrigin = `http://localhost:${CDN_PORT}`
const transformMode = process.env.TRANSFORM_ASSET_URLS_MODE || 'string'
const transformMode = process.env.TRANSFORM_ASSETS_MODE || 'string'
const optionsKind =
process.env.TRANSFORM_ASSET_URLS_OPTIONS_KIND || 'createTransform'
const optionsCache = process.env.TRANSFORM_ASSET_URLS_OPTIONS_CACHE || 'true'
const optionsWarmup = process.env.TRANSFORM_ASSET_URLS_OPTIONS_WARMUP || 'true'
process.env.TRANSFORM_ASSETS_OPTIONS_KIND || 'createTransform'
const optionsCache = process.env.TRANSFORM_ASSETS_OPTIONS_CACHE || 'true'
const optionsWarmup = process.env.TRANSFORM_ASSETS_OPTIONS_WARMUP || 'true'
const useDeprecatedTransformAssetUrls =
process.env.USE_DEPRECATED_TRANSFORM_ASSET_URLS || 'false'

export default defineConfig({
testDir: './tests',
Expand All @@ -35,18 +37,19 @@ export default defineConfig({
},
{
// App server — builds the project then starts the srvx server
// with CDN_ORIGIN so that transformAssetUrls rewrites manifest URLs
// with CDN_ORIGIN so that transformAssets rewrites manifest URLs
command: `pnpm build && pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
PORT: String(APP_PORT),
CDN_ORIGIN: cdnOrigin,
TRANSFORM_ASSET_URLS_MODE: transformMode,
TRANSFORM_ASSET_URLS_OPTIONS_KIND: optionsKind,
TRANSFORM_ASSET_URLS_OPTIONS_CACHE: optionsCache,
TRANSFORM_ASSET_URLS_OPTIONS_WARMUP: optionsWarmup,
TRANSFORM_ASSETS_MODE: transformMode,
TRANSFORM_ASSETS_OPTIONS_KIND: optionsKind,
TRANSFORM_ASSETS_OPTIONS_CACHE: optionsCache,
TRANSFORM_ASSETS_OPTIONS_WARMUP: optionsWarmup,
USE_DEPRECATED_TRANSFORM_ASSET_URLS: useDeprecatedTransformAssetUrls,
},
timeout: 120_000,
},
Expand Down
7 changes: 6 additions & 1 deletion e2e/react-start/transform-asset-urls/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ function RootComponent() {
return (
<html>
<head>
<HeadContent />
<HeadContent
assetCrossOrigin={{
modulepreload: 'anonymous',
stylesheet: 'use-credentials',
}}
/>
</head>
<body>
<div className="app-styled">
Expand Down
2 changes: 1 addition & 1 deletion e2e/react-start/transform-asset-urls/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function Home() {
<div>
<h1 data-testid="home-heading">Welcome Home</h1>
<p data-testid="home-content">
This page tests the transformAssetUrls feature.
This page tests the transformAssets feature.
</p>
<Link to="/about" data-testid="link-to-about">
About
Expand Down
137 changes: 106 additions & 31 deletions e2e/react-start/transform-asset-urls/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,127 @@ import {
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createServerEntry } from '@tanstack/react-start/server-entry'
import type { TransformAssetUrls } from '@tanstack/react-start/server'

type TransformAssetsFn = (ctx: {
kind: 'modulepreload' | 'stylesheet' | 'clientEntry'
url: string
}) =>
| string
| {
href: string
crossOrigin?: 'anonymous' | 'use-credentials'
}

const cdnOrigin = process.env.CDN_ORIGIN
const transformMode = process.env.TRANSFORM_ASSET_URLS_MODE || 'string'
const transformMode = process.env.TRANSFORM_ASSETS_MODE || 'string'
const optionsKind =
process.env.TRANSFORM_ASSET_URLS_OPTIONS_KIND || 'createTransform'
const optionsCache = process.env.TRANSFORM_ASSET_URLS_OPTIONS_CACHE || 'true'
const optionsWarmup = process.env.TRANSFORM_ASSET_URLS_OPTIONS_WARMUP || 'true'
process.env.TRANSFORM_ASSETS_OPTIONS_KIND || 'createTransform'
const optionsCache = process.env.TRANSFORM_ASSETS_OPTIONS_CACHE || 'true'
const optionsWarmup = process.env.TRANSFORM_ASSETS_OPTIONS_WARMUP || 'true'
const useDeprecatedTransformAssetUrls =
process.env.USE_DEPRECATED_TRANSFORM_ASSET_URLS === 'true'

const cache = optionsCache !== 'false'
const warmup = optionsWarmup === 'true'

console.log(
`[server-entry]: using custom server entry with transformAssetUrls (${transformMode}${transformMode === 'options' ? `:${optionsKind}` : ''})${cdnOrigin ? ` (CDN: ${cdnOrigin})` : ' (no CDN)'}`,
`[server-entry]: using custom server entry with ${useDeprecatedTransformAssetUrls ? 'transformAssetUrls' : 'transformAssets'} (${transformMode}${transformMode === 'options' ? `:${optionsKind}` : ''})${cdnOrigin ? ` (CDN: ${cdnOrigin})` : ' (no CDN)'}`,
)

const createTransformAssetsFn =
(cdn: string): TransformAssetsFn =>
({ kind, url }) => {
const href = `${cdn}${url}`

if (kind === 'modulepreload') {
return {
href,
crossOrigin: 'anonymous',
}
}

if (kind === 'stylesheet') {
return {
href,
crossOrigin: 'anonymous',
}
}

return { href }
}

const createTransformAssetsConfig = (cdn: string) => {
const transformAssetsFn = createTransformAssetsFn(cdn)

if (transformMode === 'function') {
return transformAssetsFn
}

if (transformMode === 'options') {
if (optionsKind === 'transform') {
return {
transform: transformAssetsFn,
cache,
warmup,
}
}

return {
createTransform: async () => {
return transformAssetsFn
},
cache,
warmup,
}
}

return cdn
}

const createDeprecatedTransformAssetUrlsConfig = (
cdn: string,
): TransformAssetUrls => {
if (transformMode === 'function') {
return ({ url }) => `${cdn}${url}`
}

if (transformMode === 'options') {
if (optionsKind === 'transform') {
return {
transform: ({ url }) => `${cdn}${url}`,
cache,
warmup,
}
}

return {
createTransform: async () => {
return ({ url }) => `${cdn}${url}`
},
cache,
warmup,
}
}

return cdn
}

const handler = createStartHandler(
cdnOrigin
? {
handler: defaultStreamHandler,
transformAssetUrls: (() => {
const cdn = cdnOrigin.replace(/\/+$/, '')

if (transformMode === 'function') {
return ({ url }: { url: string }) => `${cdn}${url}`
}

if (transformMode === 'options') {
if (optionsKind === 'transform') {
return {
transform: ({ url }: { url: string }) => `${cdn}${url}`,
cache,
warmup,
}
...(useDeprecatedTransformAssetUrls
? {
transformAssetUrls: createDeprecatedTransformAssetUrlsConfig(
cdnOrigin.replace(/\/+$/, ''),
),
}

return {
createTransform: async () => {
return ({ url }: { url: string }) => `${cdn}${url}`
},
cache,
warmup,
}
}

return cdn
})(),
: {
transformAssets: createTransformAssetsConfig(
cdnOrigin.replace(/\/+$/, ''),
),
}),
}
: defaultStreamHandler,
)
Expand Down
17 changes: 15 additions & 2 deletions e2e/react-start/transform-asset-urls/tests/cdn-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,22 @@ app.get('/health', (_req, res) => {
res.status(200).send('ok')
})

// Serve the built client assets with CORS headers to simulate a CDN
// Serve the built client assets with CORS headers to simulate a CDN.
// Origin reflection is intentional for this test server: the e2e tests use
// crossorigin="use-credentials" which requires Access-Control-Allow-Origin
// to echo the requesting origin (wildcard '*' is not allowed with credentials).
// Do NOT copy this pattern for production — validate origins against an allowlist.
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
const origin = req.headers.origin

if (origin) {
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Vary', 'Origin')
} else {
res.setHeader('Access-Control-Allow-Origin', '*')
}

res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
next()
Expand Down
Loading
Loading