@tanstack/start-server-core 1.158.3 → 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.
- package/dist/esm/createStartHandler.d.ts +86 -1
- package/dist/esm/createStartHandler.js +68 -8
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/router-manifest.d.ts +7 -10
- package/dist/esm/router-manifest.js +9 -13
- package/dist/esm/router-manifest.js.map +1 -1
- package/dist/esm/transformAssetUrls.d.ts +115 -0
- package/dist/esm/transformAssetUrls.js +113 -0
- package/dist/esm/transformAssetUrls.js.map +1 -0
- package/package.json +4 -4
- package/src/createStartHandler.ts +231 -9
- package/src/index.tsx +9 -0
- package/src/router-manifest.ts +17 -17
- package/src/transformAssetUrls.ts +285 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { rootRouteId } from "@tanstack/router-core";
|
|
2
|
+
function resolveTransformConfig(transform) {
|
|
3
|
+
if (typeof transform === "string") {
|
|
4
|
+
const prefix = transform;
|
|
5
|
+
return {
|
|
6
|
+
type: "transform",
|
|
7
|
+
transformFn: ({ url }) => `${prefix}${url}`,
|
|
8
|
+
cache: true
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (typeof transform === "function") {
|
|
12
|
+
return {
|
|
13
|
+
type: "transform",
|
|
14
|
+
transformFn: transform,
|
|
15
|
+
cache: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if ("createTransform" in transform && transform.createTransform) {
|
|
19
|
+
return {
|
|
20
|
+
type: "createTransform",
|
|
21
|
+
createTransform: transform.createTransform,
|
|
22
|
+
cache: transform.cache !== false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const transformFn = typeof transform.transform === "string" ? (({ url }) => `${transform.transform}${url}`) : transform.transform;
|
|
26
|
+
return {
|
|
27
|
+
type: "transform",
|
|
28
|
+
transformFn,
|
|
29
|
+
cache: transform.cache !== false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function buildClientEntryScriptTag(clientEntry, injectedHeadScripts) {
|
|
33
|
+
const clientEntryLiteral = JSON.stringify(clientEntry);
|
|
34
|
+
let script = `import(${clientEntryLiteral})`;
|
|
35
|
+
if (injectedHeadScripts) {
|
|
36
|
+
script = `${injectedHeadScripts};${script}`;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
tag: "script",
|
|
40
|
+
attrs: {
|
|
41
|
+
type: "module",
|
|
42
|
+
async: true
|
|
43
|
+
},
|
|
44
|
+
children: script
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function transformManifestUrls(source, transformFn, opts) {
|
|
48
|
+
return (async () => {
|
|
49
|
+
const manifest = opts?.clone ? structuredClone(source.manifest) : source.manifest;
|
|
50
|
+
for (const route of Object.values(manifest.routes)) {
|
|
51
|
+
if (route.preloads) {
|
|
52
|
+
route.preloads = await Promise.all(
|
|
53
|
+
route.preloads.map(
|
|
54
|
+
(url) => Promise.resolve(transformFn({ url, type: "modulepreload" }))
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (route.assets) {
|
|
59
|
+
for (const asset of route.assets) {
|
|
60
|
+
if (asset.tag === "link" && asset.attrs?.href) {
|
|
61
|
+
asset.attrs.href = await Promise.resolve(
|
|
62
|
+
transformFn({
|
|
63
|
+
url: asset.attrs.href,
|
|
64
|
+
type: "stylesheet"
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const transformedClientEntry = await Promise.resolve(
|
|
72
|
+
transformFn({
|
|
73
|
+
url: source.clientEntry,
|
|
74
|
+
type: "clientEntry"
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
const rootRoute = manifest.routes[rootRouteId];
|
|
78
|
+
if (rootRoute) {
|
|
79
|
+
rootRoute.assets = rootRoute.assets || [];
|
|
80
|
+
rootRoute.assets.push(
|
|
81
|
+
buildClientEntryScriptTag(
|
|
82
|
+
transformedClientEntry,
|
|
83
|
+
source.injectedHeadScripts
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return manifest;
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
function buildManifestWithClientEntry(source) {
|
|
91
|
+
const scriptTag = buildClientEntryScriptTag(
|
|
92
|
+
source.clientEntry,
|
|
93
|
+
source.injectedHeadScripts
|
|
94
|
+
);
|
|
95
|
+
const baseRootRoute = source.manifest.routes[rootRouteId];
|
|
96
|
+
const routes = {
|
|
97
|
+
...source.manifest.routes,
|
|
98
|
+
...baseRootRoute ? {
|
|
99
|
+
[rootRouteId]: {
|
|
100
|
+
...baseRootRoute,
|
|
101
|
+
assets: [...baseRootRoute.assets || [], scriptTag]
|
|
102
|
+
}
|
|
103
|
+
} : {}
|
|
104
|
+
};
|
|
105
|
+
return { routes };
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
buildClientEntryScriptTag,
|
|
109
|
+
buildManifestWithClientEntry,
|
|
110
|
+
resolveTransformConfig,
|
|
111
|
+
transformManifestUrls
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=transformAssetUrls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformAssetUrls.js","sources":["../../src/transformAssetUrls.ts"],"sourcesContent":["import { rootRouteId } from '@tanstack/router-core'\n\nimport type {\n Awaitable,\n Manifest,\n RouterManagedTag,\n} from '@tanstack/router-core'\n\nexport type AssetUrlType = 'modulepreload' | 'stylesheet' | 'clientEntry'\n\nexport interface TransformAssetUrlsContext {\n url: string\n type: AssetUrlType\n}\n\nexport type TransformAssetUrlsFn = (\n context: TransformAssetUrlsContext,\n) => Awaitable<string>\n\nexport type CreateTransformAssetUrlsContext =\n | {\n /** True when the server is computing the cached manifest during startup warmup. */\n warmup: true\n }\n | {\n /**\n * The current Request.\n *\n * Only available during request handling (i.e. when `warmup: false`).\n */\n request: Request\n /** False when transforming URLs as part of request handling. */\n warmup: false\n }\n\n/**\n * Async factory that runs once per manifest computation and returns the\n * per-asset transform.\n */\nexport type CreateTransformAssetUrlsFn = (\n ctx: CreateTransformAssetUrlsContext,\n) => Awaitable<TransformAssetUrlsFn>\n\ntype TransformAssetUrlsOptionsBase = {\n /**\n * Whether to cache the transformed manifest after the first request.\n *\n * When `true` (default), the transform runs once on the first request and\n * the resulting manifest is reused for all subsequent requests in production.\n *\n * Set to `false` for per-request transforms (e.g. geo-routing to different\n * CDNs based on request headers).\n *\n * @default true\n */\n cache?: boolean\n\n /**\n * When `true`, warms up the cached transformed manifest in the background when\n * the server starts (production only).\n *\n * This can reduce latency for the first request when `cache` is `true`.\n * Has no effect when `cache: false` (per-request transforms) or in dev mode.\n *\n * @default false\n */\n warmup?: boolean\n}\n\nexport type TransformAssetUrlsOptions =\n | (TransformAssetUrlsOptionsBase & {\n /**\n * The transform to apply to asset URLs. Can be a string prefix or a callback.\n *\n * **String** — prepended to every asset URL.\n * **Callback** — receives `{ url, type }` and returns a new URL.\n */\n transform: string | TransformAssetUrlsFn\n createTransform?: never\n })\n | (TransformAssetUrlsOptionsBase & {\n /**\n * Create a per-asset transform function.\n *\n * This factory runs once per manifest computation (per request when\n * `cache: false`, or once per server when `cache: true`). It can do async\n * setup work (fetch config, read from a KV, etc.) and return a fast\n * per-asset transformer.\n */\n createTransform: CreateTransformAssetUrlsFn\n transform?: never\n })\n\nexport type TransformAssetUrls =\n | string\n | TransformAssetUrlsFn\n | TransformAssetUrlsOptions\n\nexport type ResolvedTransformAssetUrlsConfig =\n | {\n type: 'transform'\n transformFn: TransformAssetUrlsFn\n cache: boolean\n }\n | {\n type: 'createTransform'\n createTransform: CreateTransformAssetUrlsFn\n cache: boolean\n }\n\n/**\n * Resolves a TransformAssetUrls value (string prefix, callback, or options\n * object) into a concrete transform function and cache flag.\n */\nexport function resolveTransformConfig(\n transform: TransformAssetUrls,\n): ResolvedTransformAssetUrlsConfig {\n // String shorthand\n if (typeof transform === 'string') {\n const prefix = transform\n return {\n type: 'transform',\n transformFn: ({ url }) => `${prefix}${url}`,\n cache: true,\n }\n }\n\n // Callback shorthand\n if (typeof transform === 'function') {\n return {\n type: 'transform',\n transformFn: transform,\n cache: true,\n }\n }\n\n // Options object\n if ('createTransform' in transform && transform.createTransform) {\n return {\n type: 'createTransform',\n createTransform: transform.createTransform,\n cache: transform.cache !== false,\n }\n }\n\n const transformFn =\n typeof transform.transform === 'string'\n ? ((({ url }: TransformAssetUrlsContext) =>\n `${transform.transform}${url}`) as TransformAssetUrlsFn)\n : transform.transform\n\n return {\n type: 'transform',\n transformFn,\n cache: transform.cache !== false,\n }\n}\n\nexport interface StartManifestWithClientEntry {\n manifest: Manifest\n clientEntry: string\n /** Script content prepended before the client entry import (dev only) */\n injectedHeadScripts?: string\n}\n\n/**\n * Builds the client entry `<script>` tag from a (possibly transformed) client\n * entry URL and optional injected head scripts.\n */\nexport function buildClientEntryScriptTag(\n clientEntry: string,\n injectedHeadScripts?: string,\n): RouterManagedTag {\n const clientEntryLiteral = JSON.stringify(clientEntry)\n let script = `import(${clientEntryLiteral})`\n if (injectedHeadScripts) {\n script = `${injectedHeadScripts};${script}`\n }\n return {\n tag: 'script',\n attrs: {\n type: 'module',\n async: true,\n },\n children: script,\n }\n}\n\n/**\n * Applies a URL transform to every asset URL in the manifest and returns a\n * new manifest with a client entry script tag appended to the root route's\n * assets.\n *\n * The source manifest is deep-cloned so the cached original is never mutated.\n */\nexport function transformManifestUrls(\n source: StartManifestWithClientEntry,\n transformFn: TransformAssetUrlsFn,\n opts?: {\n /** When true, clone the source manifest before mutating it. */\n clone?: boolean\n },\n): Promise<Manifest> {\n return (async () => {\n const manifest = opts?.clone\n ? structuredClone(source.manifest)\n : source.manifest\n\n for (const route of Object.values(manifest.routes)) {\n // Transform preload URLs (modulepreload)\n if (route.preloads) {\n route.preloads = await Promise.all(\n route.preloads.map((url) =>\n Promise.resolve(transformFn({ url, type: 'modulepreload' })),\n ),\n )\n }\n\n // Transform asset tag URLs\n if (route.assets) {\n for (const asset of route.assets) {\n if (asset.tag === 'link' && asset.attrs?.href) {\n asset.attrs.href = await Promise.resolve(\n transformFn({\n url: asset.attrs.href,\n type: 'stylesheet',\n }),\n )\n }\n }\n }\n }\n\n // Transform and append the client entry script tag\n const transformedClientEntry = await Promise.resolve(\n transformFn({\n url: source.clientEntry,\n type: 'clientEntry',\n }),\n )\n\n const rootRoute = manifest.routes[rootRouteId]\n if (rootRoute) {\n rootRoute.assets = rootRoute.assets || []\n rootRoute.assets.push(\n buildClientEntryScriptTag(\n transformedClientEntry,\n source.injectedHeadScripts,\n ),\n )\n }\n\n return manifest\n })()\n}\n\n/**\n * Builds a final Manifest from a StartManifestWithClientEntry without any\n * URL transforms. Used when no transformAssetUrls option is provided.\n *\n * Returns a new manifest object so the cached base manifest is never mutated.\n */\nexport function buildManifestWithClientEntry(\n source: StartManifestWithClientEntry,\n): Manifest {\n const scriptTag = buildClientEntryScriptTag(\n source.clientEntry,\n source.injectedHeadScripts,\n )\n\n const baseRootRoute = source.manifest.routes[rootRouteId]\n const routes = {\n ...source.manifest.routes,\n ...(baseRootRoute\n ? {\n [rootRouteId]: {\n ...baseRootRoute,\n assets: [...(baseRootRoute.assets || []), scriptTag],\n },\n }\n : {}),\n }\n\n return { routes }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/start-server-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.159.0",
|
|
4
4
|
"description": "Modern and scalable routing for React applications",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"seroval": "^1.4.2",
|
|
65
65
|
"tiny-invariant": "^1.3.3",
|
|
66
66
|
"@tanstack/history": "1.154.14",
|
|
67
|
-
"@tanstack/
|
|
68
|
-
"@tanstack/
|
|
69
|
-
"@tanstack/start-
|
|
67
|
+
"@tanstack/router-core": "1.158.4",
|
|
68
|
+
"@tanstack/start-client-core": "1.158.4",
|
|
69
|
+
"@tanstack/start-storage-context": "1.158.4"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@standard-schema/spec": "^1.0.0",
|
|
@@ -19,6 +19,11 @@ import { runWithStartContext } from '@tanstack/start-storage-context'
|
|
|
19
19
|
import { requestHandler } from './request-response'
|
|
20
20
|
import { getStartManifest } from './router-manifest'
|
|
21
21
|
import { handleServerAction } from './server-functions-handler'
|
|
22
|
+
import {
|
|
23
|
+
buildManifestWithClientEntry,
|
|
24
|
+
resolveTransformConfig,
|
|
25
|
+
transformManifestUrls,
|
|
26
|
+
} from './transformAssetUrls'
|
|
22
27
|
|
|
23
28
|
import { HEADERS } from './constants'
|
|
24
29
|
import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
|
|
@@ -39,6 +44,11 @@ import type {
|
|
|
39
44
|
Register,
|
|
40
45
|
} from '@tanstack/router-core'
|
|
41
46
|
import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
|
|
47
|
+
import type {
|
|
48
|
+
StartManifestWithClientEntry,
|
|
49
|
+
TransformAssetUrls,
|
|
50
|
+
TransformAssetUrlsFn,
|
|
51
|
+
} from './transformAssetUrls'
|
|
42
52
|
|
|
43
53
|
type TODO = any
|
|
44
54
|
|
|
@@ -46,6 +56,61 @@ type AnyMiddlewareServerFn =
|
|
|
46
56
|
| AnyRequestMiddleware['options']['server']
|
|
47
57
|
| AnyFunctionMiddleware['options']['server']
|
|
48
58
|
|
|
59
|
+
export interface CreateStartHandlerOptions {
|
|
60
|
+
handler: HandlerCallback<AnyRouter>
|
|
61
|
+
/**
|
|
62
|
+
* Transform asset URLs at runtime, e.g. to prepend a CDN prefix.
|
|
63
|
+
*
|
|
64
|
+
* **String** — a URL prefix prepended to every asset URL (cached by default):
|
|
65
|
+
* ```ts
|
|
66
|
+
* createStartHandler({
|
|
67
|
+
* handler: defaultStreamHandler,
|
|
68
|
+
* transformAssetUrls: 'https://cdn.example.com',
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* **Callback** — receives `{ url, type }` and returns a new URL
|
|
73
|
+
* (cached by default — runs once on first request):
|
|
74
|
+
* ```ts
|
|
75
|
+
* createStartHandler({
|
|
76
|
+
* handler: defaultStreamHandler,
|
|
77
|
+
* transformAssetUrls: ({ url, type }) => {
|
|
78
|
+
* return `https://cdn.example.com${url}`
|
|
79
|
+
* },
|
|
80
|
+
* })
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* **Object** — for explicit cache control:
|
|
84
|
+
* ```ts
|
|
85
|
+
* createStartHandler({
|
|
86
|
+
* handler: defaultStreamHandler,
|
|
87
|
+
* transformAssetUrls: {
|
|
88
|
+
* transform: ({ url }) => {
|
|
89
|
+
* const region = getRequest().headers.get('x-region') || 'us'
|
|
90
|
+
* return `https://cdn-${region}.example.com${url}`
|
|
91
|
+
* },
|
|
92
|
+
* cache: false, // transform per-request
|
|
93
|
+
* },
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* `type` is one of `'modulepreload' | 'stylesheet' | 'clientEntry'`.
|
|
98
|
+
*
|
|
99
|
+
* By default, the transformed manifest is cached after the first request
|
|
100
|
+
* (`cache: true`). Set `cache: false` for per-request transforms.
|
|
101
|
+
*
|
|
102
|
+
* If you're using a cached transform, you can optionally set `warmup: true`
|
|
103
|
+
* (object form only) to compute the transformed manifest in the background at
|
|
104
|
+
* server startup.
|
|
105
|
+
*
|
|
106
|
+
* Note: This only transforms URLs managed by TanStack Start's manifest
|
|
107
|
+
* (JS preloads, CSS links, and the client entry script). For asset imports
|
|
108
|
+
* used directly in components (e.g. `import logo from './logo.svg'`),
|
|
109
|
+
* configure Vite's `experimental.renderBuiltUrl` in your vite.config.ts.
|
|
110
|
+
*/
|
|
111
|
+
transformAssetUrls?: TransformAssetUrls
|
|
112
|
+
}
|
|
113
|
+
|
|
49
114
|
function getStartResponseHeaders(opts: { router: AnyRouter }) {
|
|
50
115
|
const headers = mergeHeaders(
|
|
51
116
|
{
|
|
@@ -66,7 +131,13 @@ let entriesPromise:
|
|
|
66
131
|
routerEntry: RouterEntry
|
|
67
132
|
}>
|
|
68
133
|
| undefined
|
|
69
|
-
let
|
|
134
|
+
let baseManifestPromise: Promise<StartManifestWithClientEntry> | undefined
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Cached final manifest (with client entry script tag). In production,
|
|
138
|
+
* this is computed once and reused for every request when caching is enabled.
|
|
139
|
+
*/
|
|
140
|
+
let cachedFinalManifestPromise: Promise<Manifest> | undefined
|
|
70
141
|
|
|
71
142
|
async function loadEntries() {
|
|
72
143
|
// @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
|
|
@@ -83,16 +154,59 @@ function getEntries() {
|
|
|
83
154
|
return entriesPromise
|
|
84
155
|
}
|
|
85
156
|
|
|
86
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Returns the raw manifest data (without client entry script tag baked in).
|
|
159
|
+
* In dev mode, always returns fresh data. In prod, cached.
|
|
160
|
+
*/
|
|
161
|
+
function getBaseManifest(
|
|
162
|
+
matchedRoutes?: ReadonlyArray<AnyRoute>,
|
|
163
|
+
): Promise<StartManifestWithClientEntry> {
|
|
87
164
|
// In dev mode, always get fresh manifest (no caching) to include route-specific dev styles
|
|
88
165
|
if (process.env.TSS_DEV_SERVER === 'true') {
|
|
89
166
|
return getStartManifest(matchedRoutes)
|
|
90
167
|
}
|
|
91
|
-
// In prod, cache the manifest
|
|
92
|
-
if (!
|
|
93
|
-
|
|
168
|
+
// In prod, cache the base manifest
|
|
169
|
+
if (!baseManifestPromise) {
|
|
170
|
+
baseManifestPromise = getStartManifest()
|
|
94
171
|
}
|
|
95
|
-
return
|
|
172
|
+
return baseManifestPromise
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolves a final Manifest for a given request.
|
|
177
|
+
*
|
|
178
|
+
* - No transform: builds client entry script tag and returns (cached in prod).
|
|
179
|
+
* - Cached transform: transforms all URLs + builds script tag, caches result.
|
|
180
|
+
* - Per-request transform: deep-clones base manifest, transforms per-request.
|
|
181
|
+
*/
|
|
182
|
+
async function resolveManifest(
|
|
183
|
+
matchedRoutes: ReadonlyArray<AnyRoute> | undefined,
|
|
184
|
+
transformFn: TransformAssetUrlsFn | undefined,
|
|
185
|
+
cache: boolean,
|
|
186
|
+
): Promise<Manifest> {
|
|
187
|
+
const base = await getBaseManifest(matchedRoutes)
|
|
188
|
+
|
|
189
|
+
const computeFinalManifest = async () => {
|
|
190
|
+
return transformFn
|
|
191
|
+
? await transformManifestUrls(base, transformFn, { clone: !cache })
|
|
192
|
+
: buildManifestWithClientEntry(base)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// In dev, always compute fresh to include route-specific dev styles.
|
|
196
|
+
if (process.env.TSS_DEV_SERVER === 'true') {
|
|
197
|
+
return computeFinalManifest()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// In prod, cache unless we're explicitly doing per-request transforms.
|
|
201
|
+
if (!transformFn || cache) {
|
|
202
|
+
if (!cachedFinalManifestPromise) {
|
|
203
|
+
cachedFinalManifestPromise = computeFinalManifest()
|
|
204
|
+
}
|
|
205
|
+
return cachedFinalManifestPromise
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Per-request transform — deep-clone and transform every time.
|
|
209
|
+
return computeFinalManifest()
|
|
96
210
|
}
|
|
97
211
|
|
|
98
212
|
// Pre-computed constants
|
|
@@ -206,9 +320,107 @@ function handlerToMiddleware(
|
|
|
206
320
|
}
|
|
207
321
|
}
|
|
208
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Creates the TanStack Start request handler.
|
|
325
|
+
*
|
|
326
|
+
* @example Backwards-compatible usage (handler callback only):
|
|
327
|
+
* ```ts
|
|
328
|
+
* export default createStartHandler(defaultStreamHandler)
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* @example With CDN URL rewriting:
|
|
332
|
+
* ```ts
|
|
333
|
+
* export default createStartHandler({
|
|
334
|
+
* handler: defaultStreamHandler,
|
|
335
|
+
* transformAssetUrls: 'https://cdn.example.com',
|
|
336
|
+
* })
|
|
337
|
+
* ```
|
|
338
|
+
*
|
|
339
|
+
* @example With per-request URL rewriting:
|
|
340
|
+
* ```ts
|
|
341
|
+
* export default createStartHandler({
|
|
342
|
+
* handler: defaultStreamHandler,
|
|
343
|
+
* transformAssetUrls: {
|
|
344
|
+
* transform: ({ url }) => {
|
|
345
|
+
* const cdnBase = getRequest().headers.get('x-cdn-base') || ''
|
|
346
|
+
* return `${cdnBase}${url}`
|
|
347
|
+
* },
|
|
348
|
+
* cache: false,
|
|
349
|
+
* },
|
|
350
|
+
* })
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
209
353
|
export function createStartHandler<TRegister = Register>(
|
|
210
|
-
|
|
354
|
+
cbOrOptions: HandlerCallback<AnyRouter> | CreateStartHandlerOptions,
|
|
211
355
|
): RequestHandler<TRegister> {
|
|
356
|
+
// Normalize the overloaded argument
|
|
357
|
+
const cb: HandlerCallback<AnyRouter> =
|
|
358
|
+
typeof cbOrOptions === 'function' ? cbOrOptions : cbOrOptions.handler
|
|
359
|
+
const transformAssetUrlsOption: TransformAssetUrls | undefined =
|
|
360
|
+
typeof cbOrOptions === 'function'
|
|
361
|
+
? undefined
|
|
362
|
+
: cbOrOptions.transformAssetUrls
|
|
363
|
+
|
|
364
|
+
const warmupTransformManifest =
|
|
365
|
+
!!transformAssetUrlsOption &&
|
|
366
|
+
typeof transformAssetUrlsOption === 'object' &&
|
|
367
|
+
transformAssetUrlsOption.warmup === true
|
|
368
|
+
|
|
369
|
+
// Pre-resolve the transform function and cache flag
|
|
370
|
+
const resolvedTransformConfig = transformAssetUrlsOption
|
|
371
|
+
? resolveTransformConfig(transformAssetUrlsOption)
|
|
372
|
+
: undefined
|
|
373
|
+
const cache = resolvedTransformConfig ? resolvedTransformConfig.cache : true
|
|
374
|
+
|
|
375
|
+
// Memoize a single createTransform() result when caching is enabled.
|
|
376
|
+
let cachedCreateTransformPromise: Promise<TransformAssetUrlsFn> | undefined
|
|
377
|
+
|
|
378
|
+
const getTransformFn = async (
|
|
379
|
+
opts: { warmup: true } | { warmup: false; request: Request },
|
|
380
|
+
): Promise<TransformAssetUrlsFn | undefined> => {
|
|
381
|
+
if (!resolvedTransformConfig) return undefined
|
|
382
|
+
if (resolvedTransformConfig.type === 'createTransform') {
|
|
383
|
+
if (cache) {
|
|
384
|
+
if (!cachedCreateTransformPromise) {
|
|
385
|
+
cachedCreateTransformPromise = Promise.resolve(
|
|
386
|
+
resolvedTransformConfig.createTransform(opts),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
return cachedCreateTransformPromise
|
|
390
|
+
}
|
|
391
|
+
return resolvedTransformConfig.createTransform(opts)
|
|
392
|
+
}
|
|
393
|
+
return resolvedTransformConfig.transformFn
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Background warmup for cached transforms (production only)
|
|
397
|
+
if (
|
|
398
|
+
warmupTransformManifest &&
|
|
399
|
+
cache &&
|
|
400
|
+
process.env.TSS_DEV_SERVER !== 'true' &&
|
|
401
|
+
!cachedFinalManifestPromise
|
|
402
|
+
) {
|
|
403
|
+
// NOTE: Do not call resolveManifest() here.
|
|
404
|
+
// resolveManifest() reads from cachedFinalManifestPromise, and since we set
|
|
405
|
+
// cachedFinalManifestPromise to this warmup promise, that would create a
|
|
406
|
+
// self-referential promise and hang forever.
|
|
407
|
+
const warmupPromise = (async () => {
|
|
408
|
+
const base = await getBaseManifest(undefined)
|
|
409
|
+
const transformFn = await getTransformFn({ warmup: true })
|
|
410
|
+
return transformFn
|
|
411
|
+
? await transformManifestUrls(base, transformFn, { clone: false })
|
|
412
|
+
: buildManifestWithClientEntry(base)
|
|
413
|
+
})()
|
|
414
|
+
cachedFinalManifestPromise = warmupPromise
|
|
415
|
+
warmupPromise.catch(() => {
|
|
416
|
+
// If warmup fails, allow the next request to retry.
|
|
417
|
+
if (cachedFinalManifestPromise === warmupPromise) {
|
|
418
|
+
cachedFinalManifestPromise = undefined
|
|
419
|
+
}
|
|
420
|
+
cachedCreateTransformPromise = undefined
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
212
424
|
const startRequestResolver: RequestHandler<Register> = async (
|
|
213
425
|
request,
|
|
214
426
|
requestOpts,
|
|
@@ -218,10 +430,16 @@ export function createStartHandler<TRegister = Register>(
|
|
|
218
430
|
|
|
219
431
|
try {
|
|
220
432
|
// normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
|
|
221
|
-
|
|
433
|
+
// during normalization paths like '//posts' are flattened to '/posts'.
|
|
434
|
+
// in these cases we would prefer to redirect to the new path
|
|
435
|
+
const { url, handledProtocolRelativeURL } = getNormalizedURL(request.url)
|
|
222
436
|
const href = url.pathname + url.search + url.hash
|
|
223
437
|
const origin = getOrigin(request)
|
|
224
438
|
|
|
439
|
+
if (handledProtocolRelativeURL) {
|
|
440
|
+
return Response.redirect(url, 308)
|
|
441
|
+
}
|
|
442
|
+
|
|
225
443
|
const entries = await getEntries()
|
|
226
444
|
const startOptions: AnyStartInstanceOptions =
|
|
227
445
|
(await entries.startEntry.startInstance?.getOptions()) ||
|
|
@@ -339,7 +557,11 @@ export function createStartHandler<TRegister = Register>(
|
|
|
339
557
|
)
|
|
340
558
|
}
|
|
341
559
|
|
|
342
|
-
const manifest = await
|
|
560
|
+
const manifest = await resolveManifest(
|
|
561
|
+
matchedRoutes,
|
|
562
|
+
await getTransformFn({ warmup: false, request }),
|
|
563
|
+
cache,
|
|
564
|
+
)
|
|
343
565
|
const routerInstance = await getRouter()
|
|
344
566
|
|
|
345
567
|
attachRouterServerSsrUtils({
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export { createStartHandler } from './createStartHandler'
|
|
2
|
+
export type { CreateStartHandlerOptions } from './createStartHandler'
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
TransformAssetUrls,
|
|
6
|
+
TransformAssetUrlsFn,
|
|
7
|
+
TransformAssetUrlsContext,
|
|
8
|
+
TransformAssetUrlsOptions,
|
|
9
|
+
AssetUrlType,
|
|
10
|
+
} from './transformAssetUrls'
|
|
2
11
|
|
|
3
12
|
export {
|
|
4
13
|
attachRouterServerSsrUtils,
|
package/src/router-manifest.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { buildDevStylesUrl, rootRouteId } from '@tanstack/router-core'
|
|
2
2
|
import type { AnyRoute, RouterManagedTag } from '@tanstack/router-core'
|
|
3
|
+
import type { StartManifestWithClientEntry } from './transformAssetUrls'
|
|
3
4
|
|
|
4
5
|
// Pre-computed constant for dev styles URL
|
|
5
6
|
const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* @description Returns the router manifest that should be sent to the client.
|
|
9
|
+
* @description Returns the router manifest data that should be sent to the client.
|
|
9
10
|
* This includes only the assets and preloads for the current route and any
|
|
10
11
|
* special assets that are needed for the client. It does not include relationships
|
|
11
12
|
* between routes or any other data that is not needed for the client.
|
|
12
13
|
*
|
|
14
|
+
* The client entry URL is returned separately so that it can be transformed
|
|
15
|
+
* (e.g. for CDN rewriting) before being embedded into the `<script>` tag.
|
|
16
|
+
*
|
|
13
17
|
* @param matchedRoutes - In dev mode, the matched routes are used to build
|
|
14
18
|
* the dev styles URL for route-scoped CSS collection.
|
|
15
19
|
*/
|
|
16
20
|
export async function getStartManifest(
|
|
17
21
|
matchedRoutes?: ReadonlyArray<AnyRoute>,
|
|
18
|
-
) {
|
|
22
|
+
): Promise<StartManifestWithClientEntry> {
|
|
19
23
|
const { tsrStartManifest } = await import('tanstack-start-manifest:v')
|
|
20
24
|
const startManifest = tsrStartManifest()
|
|
21
25
|
|
|
@@ -37,22 +41,15 @@ export async function getStartManifest(
|
|
|
37
41
|
})
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
// Collect injected head scripts in dev mode (returned separately so we can
|
|
45
|
+
// build the client entry script tag after URL transforms are applied)
|
|
46
|
+
let injectedHeadScripts: string | undefined
|
|
41
47
|
if (process.env.TSS_DEV_SERVER === 'true') {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
script = `${injectedHeadScripts + ';'}${script}`
|
|
48
|
+
const mod = await import('tanstack-start-injected-head-scripts:v')
|
|
49
|
+
if (mod.injectedHeadScripts) {
|
|
50
|
+
injectedHeadScripts = mod.injectedHeadScripts
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
|
-
rootRoute.assets.push({
|
|
49
|
-
tag: 'script',
|
|
50
|
-
attrs: {
|
|
51
|
-
type: 'module',
|
|
52
|
-
async: true,
|
|
53
|
-
},
|
|
54
|
-
children: script,
|
|
55
|
-
})
|
|
56
53
|
|
|
57
54
|
const manifest = {
|
|
58
55
|
routes: Object.fromEntries(
|
|
@@ -78,6 +75,9 @@ export async function getStartManifest(
|
|
|
78
75
|
),
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
return {
|
|
79
|
+
manifest,
|
|
80
|
+
clientEntry: startManifest.clientEntry,
|
|
81
|
+
injectedHeadScripts,
|
|
82
|
+
}
|
|
83
83
|
}
|