Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/skip-unused-start-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/start-server-core': patch
'@tanstack/start-plugin-core': patch
---

Use build-time route information to skip server-route handling for apps without a `server` option on any route. Skip the request middleware chain when none are configured, and keep early route matching when needed for early hints.
19 changes: 12 additions & 7 deletions packages/start-plugin-core/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/* eslint-disable no-var */
declare global {
var TSS_ROUTES_MANIFEST: Record<
string,
{
filePath: string
children?: Array<string>
}
>
var TSS_ROUTES_MANIFEST:
| {
routes: Record<
string,
{
filePath: string
children?: Array<string>
}
>
hasServerRoutes?: boolean
}
| undefined
var TSS_PRERENDABLE_PATHS: Array<{ path: string }> | undefined
}
export {}
22 changes: 16 additions & 6 deletions packages/start-plugin-core/src/rsbuild/start-router-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export function registerRouterPlugins(

api.modifyRspackConfig((config, utils) => {
const envName = utils.environment.name
const isBuild = api.context.action === 'build' && !config.watch
const routesPlugin = routesManifestPlugin(() => isBuild)
const { startConfig } = opts.getConfig()
const routerConfig = startConfig.router

Expand All @@ -51,7 +53,7 @@ export function registerRouterPlugins(
})
},
plugins: [
routesManifestPlugin(),
routesPlugin,
...(opts.startPluginOpts.prerender?.enabled === true
? [prerenderRoutesPlugin()]
: []),
Expand All @@ -67,15 +69,23 @@ export function registerRouterPlugins(
envName === RSBUILD_ENVIRONMENT_NAMES.server
) {
const isClient = envName === RSBUILD_ENVIRONMENT_NAMES.client
const codeSplittingOptions = {
...routerConfig.codeSplittingOptions,
deleteNodes: isClient ? ['ssr', 'server', 'headers'] : undefined,
addHmr: isClient,
compilerPlugins:
isBuild &&
isClient &&
(typeof config.cache !== 'object' ||
config.cache.type !== 'persistent')
? [routesPlugin]
: [],
}
const splitterPlugin = TanStackRouterCodeSplitterRspack(
{
...routerConfig,
target: opts.corePluginOpts.framework,
codeSplittingOptions: {
...routerConfig.codeSplittingOptions,
deleteNodes: isClient ? ['ssr', 'server', 'headers'] : undefined,
addHmr: isClient,
},
codeSplittingOptions,
},
routerPluginContext,
)
Expand Down
29 changes: 21 additions & 8 deletions packages/start-plugin-core/src/rsbuild/virtual-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ function generateManifestModuleDev(
scriptFormat: ScriptFormat,
): string {
const scriptFormatProperty = getScriptFormatProperty(scriptFormat)
return `const fallbackManifest = {
return `export const hasServerRoutes = true
const fallbackManifest = {
${scriptFormatProperty} routes: {
__root__: {
preloads: ['${devClientEntryUrl}'],
Expand All @@ -103,10 +104,12 @@ function buildStartManifestData(
inlineCss: InlineCssOptions,
scriptFormat: ScriptFormat,
) {
const routeTreeRoutes = globalThis.TSS_ROUTES_MANIFEST
const { routes: routeTreeRoutes, hasServerRoutes } =
globalThis.TSS_ROUTES_MANIFEST!
return buildStartManifest({
clientBuild,
routeTreeRoutes,
hasServerRoutes,
basePath: publicBase,
inlineCss,
scriptFormat,
Expand All @@ -133,10 +136,18 @@ function generateManifestModuleBuild(
): string {
if (!clientBuild) {
return `const tsrStartManifestData = ${JSON.stringify(START_MANIFEST_PLACEHOLDER)}
export const tsrStartManifest = () => tsrStartManifestData`
export const tsrStartManifest = () => tsrStartManifestData
export const hasServerRoutes = true`
}

return `export const tsrStartManifest = () => (${serializeStartManifestData(clientBuild, publicBase, inlineCss, scriptFormat)})`
const manifest = buildStartManifestData(
clientBuild,
publicBase,
inlineCss,
scriptFormat,
)
return `export const hasServerRoutes = ${manifest.hasServerRoutes !== false}
export const tsrStartManifest = () => (${JSON.stringify(manifest)})`
}

/**
Expand Down Expand Up @@ -389,7 +400,8 @@ export function registerVirtualModules(
opts.scriptFormat,
)
} else {
content[paths.manifest] = 'export default {}'
content[paths.manifest] =
'export const hasServerRoutes = true\nexport default {}'
}

// Server fn resolver — SSR and provider environments
Expand Down Expand Up @@ -529,13 +541,14 @@ export function createFromReadableStream() { throw new Error('RSC SSR decode is
const devClientEntryUrl = opts.getDevClientEntryUrl(
resolvedStartConfig.basePaths.publicBase,
)
if (isDev) {
return generateManifestModuleDev(devClientEntryUrl, opts.scriptFormat)
}
return generateManifestModuleBuild(
newClientBuild,
resolvedStartConfig.basePaths.publicBase,
devClientEntryUrl,
!isDev
? startConfig.server.build.inlineCss
: { enabled: false, transformAssets: false },
startConfig.server.build.inlineCss,
opts.scriptFormat,
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type DedupeRoute = {
}

export interface StartManifest {
hasServerRoutes?: boolean
scriptFormat?: ScriptFormat
routes: Record<string, RouteTreeRoute>
inlineCss?: {
Expand Down Expand Up @@ -257,6 +258,7 @@ function appendAdditionalRouteEntries(
export function buildStartManifest(options: {
clientBuild: NormalizedClientBuild
routeTreeRoutes: RouteTreeRoutes
hasServerRoutes?: boolean
basePath: string
inlineCss?: InlineCssOptions
scriptFormat?: ScriptFormat
Expand Down Expand Up @@ -298,6 +300,7 @@ export function buildStartManifest(options: {

const result: StartManifest = {
routes,
hasServerRoutes: options.hasServerRoutes,
}

if (options.scriptFormat === 'iife') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { rootRouteId } from '@tanstack/router-core'

import * as t from '@babel/types'
import { hasServerOptions } from '../pruneServerOnlySubtrees'
import { SERVER_PROP } from '../constants'
import type { CodeSplitCompilerPlugin } from '@tanstack/router-plugin'
import type { GeneratorPlugin } from '@tanstack/router-generator'

/**
* this plugin builds the routes manifest and stores it on globalThis
* so that it can be accessed later (e.g. from a vite plugin)
*/
export function routesManifestPlugin(): GeneratorPlugin {
export function routesManifestPlugin(
isBuild: () => boolean,
): GeneratorPlugin & CodeSplitCompilerPlugin {
let manifest: typeof globalThis.TSS_ROUTES_MANIFEST

return {
name: 'routes-manifest-plugin',
onRouteTreeChanged: ({ routeTree, rootRouteNode, routeNodes }) => {
const allChildren = routeTree.map((d) => d.routePath)
let hasServerRoutes: boolean | undefined = isBuild() ? undefined : true
const routes: Record<
string,
{
Expand All @@ -25,6 +33,9 @@ export function routesManifestPlugin(): GeneratorPlugin {
...Object.fromEntries(
routeNodes.map((d) => {
const filePathId = d.routePath
if (hasServerRoutes !== true && hasServerOptions(d) !== false) {
hasServerRoutes = true
}

return [
filePathId,
Expand All @@ -37,7 +48,31 @@ export function routesManifestPlugin(): GeneratorPlugin {
),
}

globalThis.TSS_ROUTES_MANIFEST = routes
manifest = { routes, hasServerRoutes }
globalThis.TSS_ROUTES_MANIFEST = manifest
},
onRouteOptions({ routeOptions, createRouteFn, opts }) {
if (!manifest || manifest.hasServerRoutes === true) {
return
}
if (
routeOptions.properties.some(
(prop) =>
t.isSpreadElement(prop) ||
prop.computed ||
t.isIdentifier(prop.key, { name: SERVER_PROP }) ||
t.isStringLiteral(prop.key, { value: SERVER_PROP }),
)
) {
manifest.hasServerRoutes = true
} else if (
opts.id ===
manifest.routes[rootRouteId]?.filePath.replaceAll('\\', '/') &&
(createRouteFn === 'createRootRoute' ||
createRouteFn === 'createRootRouteWithContext')
) {
manifest.hasServerRoutes ??= false
}
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type {
RouteNode,
} from '@tanstack/router-generator'

export function hasServerOptions(node: RouteNode) {
return node.createFileRouteProps?.has(SERVER_PROP)
}

export function pruneServerOnlySubtrees({
rootRouteNode,
acc,
Expand Down Expand Up @@ -39,8 +43,8 @@ function prune(
}

const allServerOnly =
node.createFileRouteProps?.has(SERVER_PROP) &&
node.createFileRouteProps.size === 1 &&
hasServerOptions(node) &&
node.createFileRouteProps?.size === 1 &&
allChildrenServerOnly
// prune this subtree
if (allServerOnly) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ export function devServerPlugin({

// Look up route file paths from manifest
// Only routes registered in the manifest are used - this prevents path injection
const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
| Record<string, { filePath: string; children?: Array<string> }>
| undefined
const routesManifest = globalThis.TSS_ROUTES_MANIFEST?.routes

if (routesManifest && ids.length > 0) {
for (const routeId of ids) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,18 @@ export function startManifestPlugin(opts: {
return getEmptyStartManifestModule(clientEntry)
}

const routeTreeRoutes = globalThis.TSS_ROUTES_MANIFEST
// TODO this needs further discussion with vite-rsc, this is a temporary workaround
// If the client bundle isn't available yet (e.g., during RSC scan builds),
// return a dummy manifest. The real manifest will be generated in the actual build.
if (!clientBuild) {
return getEmptyStartManifestModule(clientEntry)
}
const { routes: routeTreeRoutes, hasServerRoutes } =
globalThis.TSS_ROUTES_MANIFEST!
const startManifest = buildStartManifest({
clientBuild,
routeTreeRoutes,
hasServerRoutes,
basePath: resolvedStartConfig.basePaths.publicBase,
inlineCss: startConfig.server.build.inlineCss,
additionalRouteAssets: getViteAdditionalRouteAssets({
Expand All @@ -87,7 +89,8 @@ export function startManifestPlugin(opts: {
}),
})

return `export const tsrStartManifest = () => (${serializeStartManifest(startManifest)})`
return `export const hasServerRoutes = ${startManifest.hasServerRoutes !== false}
export const tsrStartManifest = () => (${serializeStartManifest(startManifest)})`
},
}),
]
Expand Down Expand Up @@ -139,7 +142,8 @@ function getAssetFileNameByName(
}

function getEmptyStartManifestModule(clientEntry: string) {
return `export const tsrStartManifest = () => ({
return `export const hasServerRoutes = true
export const tsrStartManifest = () => ({
routes: {
__root__: {
preloads: ['${clientEntry}'],
Expand Down
24 changes: 14 additions & 10 deletions packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { VITE_ENVIRONMENT_NAMES } from '../../constants'
import { routesManifestPlugin } from '../../start-router-plugin/generator-plugins/routes-manifest-plugin'
import { prerenderRoutesPlugin } from '../../start-router-plugin/generator-plugins/prerender-routes-plugin'
import { buildRouteTreeFileFooterFromConfig } from '../../start-router-plugin/route-tree-footer'
import { pruneServerOnlySubtrees } from '../../start-router-plugin/pruneServerOnlySubtrees'
import { SERVER_PROP } from '../../start-router-plugin/constants'
import {
hasServerOptions,
pruneServerOnlySubtrees,
} from '../../start-router-plugin/pruneServerOnlySubtrees'
import type { GetConfigFn } from '../../types'
import type { TanStackStartVitePluginCoreOptions } from '../types'
import type {
Expand All @@ -25,10 +27,7 @@ function isServerOnlyNode(node: RouteNode | undefined) {
if (!node?.createFileRouteProps) {
return false
}
return (
node.createFileRouteProps.has(SERVER_PROP) &&
node.createFileRouteProps.size === 1
)
return hasServerOptions(node) === true && node.createFileRouteProps.size === 1
}

export function tanStackStartRouter(
Expand Down Expand Up @@ -59,9 +58,10 @@ export function tanStackStartRouter(
}

let generatorInstance: Generator | null = null
let isBuild = false

const clientTreeGeneratorPlugin: GeneratorPlugin = {
name: 'start-client-tree-plugin',
const routesPlugin = {
...routesManifestPlugin(() => isBuild),
init({ generator }) {
generatorInstance = generator
},
Expand All @@ -70,7 +70,7 @@ export function tanStackStartRouter(
invalidate()
}
},
}
} satisfies GeneratorPlugin

let routeTreeFileFooter: Array<string> | null = null

Expand All @@ -96,6 +96,9 @@ export function tanStackStartRouter(
configureServer(server) {
clientEnvironment = server.environments[VITE_ENVIRONMENT_NAMES.client]
},
configResolved(config) {
isBuild = config.command === 'build' && !config.build.watch
},
config() {
type LoadObjectHook = Extract<
typeof clientTreePlugin.load,
Expand Down Expand Up @@ -146,7 +149,7 @@ export function tanStackStartRouter(
clientTreePlugin,
tanstackRouterGenerator(() => {
const routerConfig = getConfig().startConfig.router
const plugins = [clientTreeGeneratorPlugin, routesManifestPlugin()]
const plugins: Array<GeneratorPlugin> = [routesPlugin]
if (startPluginOpts.prerender?.enabled === true) {
plugins.push(prerenderRoutesPlugin())
}
Expand All @@ -165,6 +168,7 @@ export function tanStackStartRouter(
...routerConfig.codeSplittingOptions,
deleteNodes: ['ssr', 'server', 'headers'],
addHmr: true,
compilerPlugins: isBuild ? [routesPlugin] : [],
},
plugin: {
vite: { environmentName: VITE_ENVIRONMENT_NAMES.client },
Expand Down
Loading
Loading