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
8 changes: 8 additions & 0 deletions .changeset/calm-barrels-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/start-server-core': patch
'@tanstack/start-plugin-core': patch
'@tanstack/solid-start-server': patch
'@tanstack/vue-start-server': patch
---

Use focused server entrypoints for shared constants and handler helpers so build tooling and framework renderers do not traverse the full Start server barrel.
5 changes: 5 additions & 0 deletions .changeset/tidy-ravens-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-start-rsc': patch
---

Read request cancellation from the Start storage context so RSC helpers do not pull the Start server barrel into the RSC module graph.
21 changes: 21 additions & 0 deletions e2e/react-start/rsc/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { Route as RscStreamingRouteImport } from './routes/rsc-streaming'
import { Route as RscSuspenseRouteImport } from './routes/rsc-suspense'
import { Route as RscTreeRouteImport } from './routes/rsc-tree'
import { Route as RscUseServerFnRouteImport } from './routes/rsc-use-server-fn'
import { Route as ApiRscBuildBoundaryRouteImport } from './routes/api.rsc-build-boundary'
import { Route as ApiRscFlightRouteImport } from './routes/api.rsc-flight'
import { Route as RscCssConditionalIndexRouteImport } from './routes/rsc-css-conditional.index'
import { Route as RscCssConditionalBranchRouteImport } from './routes/rsc-css-conditional.$branch'
Expand Down Expand Up @@ -267,6 +268,11 @@ const RscUseServerFnRoute = RscUseServerFnRouteImport.update({
path: '/rsc-use-server-fn',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRscBuildBoundaryRoute = ApiRscBuildBoundaryRouteImport.update({
id: '/api/rsc-build-boundary',
path: '/api/rsc-build-boundary',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRscFlightRoute = ApiRscFlightRouteImport.update({
id: '/api/rsc-flight',
path: '/api/rsc-flight',
Expand Down Expand Up @@ -336,6 +342,7 @@ export interface FileRoutesByFullPath {
'/rsc-suspense': typeof RscSuspenseRoute
'/rsc-tree': typeof RscTreeRoute
'/rsc-use-server-fn': typeof RscUseServerFnRoute
'/api/rsc-build-boundary': typeof ApiRscBuildBoundaryRoute
'/api/rsc-flight': typeof ApiRscFlightRoute
'/rsc-css-conditional/$branch': typeof RscCssConditionalBranchRoute
'/rsc-param/$id': typeof RscParamIdRoute
Expand Down Expand Up @@ -385,6 +392,7 @@ export interface FileRoutesByTo {
'/rsc-suspense': typeof RscSuspenseRoute
'/rsc-tree': typeof RscTreeRoute
'/rsc-use-server-fn': typeof RscUseServerFnRoute
'/api/rsc-build-boundary': typeof ApiRscBuildBoundaryRoute
'/api/rsc-flight': typeof ApiRscFlightRoute
'/rsc-css-conditional/$branch': typeof RscCssConditionalBranchRoute
'/rsc-param/$id': typeof RscParamIdRoute
Expand Down Expand Up @@ -435,6 +443,7 @@ export interface FileRoutesById {
'/rsc-suspense': typeof RscSuspenseRoute
'/rsc-tree': typeof RscTreeRoute
'/rsc-use-server-fn': typeof RscUseServerFnRoute
'/api/rsc-build-boundary': typeof ApiRscBuildBoundaryRoute
'/api/rsc-flight': typeof ApiRscFlightRoute
'/rsc-css-conditional/$branch': typeof RscCssConditionalBranchRoute
'/rsc-param/$id': typeof RscParamIdRoute
Expand Down Expand Up @@ -486,6 +495,7 @@ export interface FileRouteTypes {
| '/rsc-suspense'
| '/rsc-tree'
| '/rsc-use-server-fn'
| '/api/rsc-build-boundary'
| '/api/rsc-flight'
| '/rsc-css-conditional/$branch'
| '/rsc-param/$id'
Expand Down Expand Up @@ -535,6 +545,7 @@ export interface FileRouteTypes {
| '/rsc-suspense'
| '/rsc-tree'
| '/rsc-use-server-fn'
| '/api/rsc-build-boundary'
| '/api/rsc-flight'
| '/rsc-css-conditional/$branch'
| '/rsc-param/$id'
Expand Down Expand Up @@ -584,6 +595,7 @@ export interface FileRouteTypes {
| '/rsc-suspense'
| '/rsc-tree'
| '/rsc-use-server-fn'
| '/api/rsc-build-boundary'
| '/api/rsc-flight'
| '/rsc-css-conditional/$branch'
| '/rsc-param/$id'
Expand Down Expand Up @@ -634,6 +646,7 @@ export interface RootRouteChildren {
RscSuspenseRoute: typeof RscSuspenseRoute
RscTreeRoute: typeof RscTreeRoute
RscUseServerFnRoute: typeof RscUseServerFnRoute
ApiRscBuildBoundaryRoute: typeof ApiRscBuildBoundaryRoute
ApiRscFlightRoute: typeof ApiRscFlightRoute
RscCssConditionalBranchRoute: typeof RscCssConditionalBranchRoute
RscParamIdRoute: typeof RscParamIdRoute
Expand Down Expand Up @@ -937,6 +950,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RscUseServerFnRouteImport
parentRoute: typeof rootRouteImport
}
'/api/rsc-build-boundary': {
id: '/api/rsc-build-boundary'
path: '/api/rsc-build-boundary'
fullPath: '/api/rsc-build-boundary'
preLoaderRoute: typeof ApiRscBuildBoundaryRouteImport
parentRoute: typeof rootRouteImport
}
'/api/rsc-flight': {
id: '/api/rsc-flight'
path: '/api/rsc-flight'
Expand Down Expand Up @@ -1018,6 +1038,7 @@ const rootRouteChildren: RootRouteChildren = {
RscSuspenseRoute: RscSuspenseRoute,
RscTreeRoute: RscTreeRoute,
RscUseServerFnRoute: RscUseServerFnRoute,
ApiRscBuildBoundaryRoute: ApiRscBuildBoundaryRoute,
ApiRscFlightRoute: ApiRscFlightRoute,
RscCssConditionalBranchRoute: RscCssConditionalBranchRoute,
RscParamIdRoute: RscParamIdRoute,
Expand Down
14 changes: 14 additions & 0 deletions e2e/react-start/rsc/src/routes/api.rsc-build-boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router'
import { getRscBuildBoundarySentinel } from '~/utils/rscBuildBoundarySentinel'

export const Route = createFileRoute('/api/rsc-build-boundary')({
server: {
handlers: {
GET: () => {
return new Response(getRscBuildBoundarySentinel(), {
headers: { 'Content-Type': 'text/plain' },
})
},
},
},
})
3 changes: 3 additions & 0 deletions e2e/react-start/rsc/src/utils/rscBuildBoundarySentinel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getRscBuildBoundarySentinel() {
return 'tanstack-start-rsc-server-only-route-sentinel'
}
87 changes: 87 additions & 0 deletions e2e/react-start/rsc/tests/rsc-build-boundary.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'

// This marker deliberately lives behind a server-only route. If its route
// dependency graph enters the RSC build, the emitted-file assertion will fail.
const SENTINEL = 'tanstack-start-rsc-server-only-route-sentinel'
const EXECUTABLE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs'])

const distDir = path.resolve(process.cwd(), process.env.E2E_DIST_DIR ?? 'dist')
const serverDir = path.join(distDir, 'server')
const rscDir = path.join(serverDir, 'rsc')

async function findExecutableFiles(
directory: string,
excludedDirectories = new Set<string>(),
): Promise<Array<string>> {
const entries = await readdir(directory, { withFileTypes: true })
const files: Array<string> = []

for (const entry of entries) {
const entryPath = path.join(directory, entry.name)

if (entry.isDirectory()) {
if (!excludedDirectories.has(entryPath)) {
files.push(
...(await findExecutableFiles(entryPath, excludedDirectories)),
)
}
} else if (
entry.isFile() &&
EXECUTABLE_EXTENSIONS.has(path.extname(entry.name))
) {
files.push(entryPath)
}
}

return files.sort()
}

async function findSentinelMatches(files: Array<string>) {
const matches: Array<string> = []

for (const file of files) {
if ((await readFile(file, 'utf-8')).includes(SENTINEL)) {
matches.push(path.relative(distDir, file))
}
}

return matches
}

test('server-only route dependencies stay out of the RSC build output', async ({
request,
}) => {
test.skip(
(process.env.E2E_TOOLCHAIN ?? 'vite') !== 'vite',
'Vite emits the RSC environment as a separate output directory',
)

const response = await request.get('/api/rsc-build-boundary')
expect(response.ok()).toBe(true)
expect(await response.text()).toBe(SENTINEL)

const [ssrFiles, rscFiles] = await Promise.all([
findExecutableFiles(serverDir, new Set([rscDir])),
findExecutableFiles(rscDir),
])
const [ssrMatches, rscMatches] = await Promise.all([
findSentinelMatches(ssrFiles),
findSentinelMatches(rscFiles),
])

expect(
ssrMatches,
`Expected the sentinel in the SSR output. Scanned files:\n${ssrFiles
.map((file) => path.relative(distDir, file))
.join('\n')}`,
).not.toEqual([])
expect(
rscMatches,
`The server-only route dependency leaked into these RSC files:\n${rscMatches.join(
'\n',
)}`,
).toEqual([])
})
27 changes: 27 additions & 0 deletions packages/react-start-rsc/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ export default [
'react-hooks/rules-of-hooks': 'error',
},
},
{
name: 'react-start-rsc/import-boundaries',
files: ['src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@tanstack/start-server-core',
message:
'Import from a dedicated @tanstack/start-server-core subpath to avoid pulling the full server barrel into the RSC module graph.',
},
],
},
],
'no-restricted-syntax': [
'error',
{
selector:
"ImportExpression[source.value='@tanstack/start-server-core']",
message:
'Dynamically import a dedicated @tanstack/start-server-core subpath instead of the root barrel.',
},
],
},
},
{
files: ['**/__tests__/**'],
rules: {
Expand Down
1 change: 0 additions & 1 deletion packages/react-start-rsc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-fn-stubs": "workspace:*",
"@tanstack/start-plugin-core": "workspace:*",
"@tanstack/start-server-core": "workspace:*",
"@tanstack/start-storage-context": "workspace:*",
"pathe": "^2.0.3"
},
Expand Down
3 changes: 1 addition & 2 deletions packages/react-start-rsc/src/createCompositeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createElement } from 'react'
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
import { getRequest } from '@tanstack/start-server-core'
import { getStartContext } from '@tanstack/start-storage-context'
import { sanitizeSlotArgs } from './slotUsageSanitizer'
import { ReplayableStream } from './ReplayableStream'
Expand Down Expand Up @@ -99,7 +98,7 @@ export async function createCompositeComponent<TComp>(

// SSR path: buffer stream for replay, pre-decode for synchronous rendering
if (isRouterRequest && ssrHandler) {
const signal = getRequest().signal
const signal = ctx.request.signal
const stream = new ReplayableStream(flightStream, { signal })

// Pre-decode during loader phase for synchronous SSR rendering
Expand Down
3 changes: 1 addition & 2 deletions packages/react-start-rsc/src/renderServerComponent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
import { getRequest } from '@tanstack/start-server-core'
import { getStartContext } from '@tanstack/start-storage-context'
import { ReplayableStream } from './ReplayableStream'
import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'
Expand Down Expand Up @@ -72,7 +71,7 @@ export async function renderServerComponent<TNode>(

// SSR path: buffer stream for replay, pre-decode for synchronous rendering
if (isRouterRequest && ssrHandler) {
const signal = getRequest().signal
const signal = ctx.request.signal
const stream = new ReplayableStream(flightStream, { signal })

// Pre-decode during loader phase for synchronous SSR rendering
Expand Down
13 changes: 1 addition & 12 deletions packages/react-start-rsc/tests/createServerComponent.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import { expectTypeOf, test, vi } from 'vitest'
import { expectTypeOf, test } from 'vitest'
import type {
CompositeComponentResult,
ValidateCompositeComponent,
} from '../src/ServerComponentTypes'
import { CompositeComponent } from '../src/CompositeComponent'

vi.mock('@tanstack/start-server-core', () => {
return {
getRequest: () => undefined,
}
})

vi.mock('@tanstack/start-storage-context', () => {
return {
getStartContext: () => undefined,
}
})
import { JSX } from 'react'

test('when a server component is created with no props', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/solid-start-server/src/defaultRenderHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineHandlerCallback } from '@tanstack/start-server-core'
import { renderRouterToString } from '@tanstack/solid-router/ssr/server'
import {
defineHandlerCallback,
renderRouterToString,
} from '@tanstack/solid-router/ssr/server'
import { StartServer } from './StartServer'

export const defaultRenderHandler = defineHandlerCallback(
Expand Down
6 changes: 4 additions & 2 deletions packages/solid-start-server/src/defaultStreamHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineHandlerCallback } from '@tanstack/start-server-core'
import { renderRouterToStream } from '@tanstack/solid-router/ssr/server'
import {
defineHandlerCallback,
renderRouterToStream,
} from '@tanstack/solid-router/ssr/server'
import { StartServer } from './StartServer'

export const defaultStreamHandler = defineHandlerCallback(
Expand Down
2 changes: 1 addition & 1 deletion packages/start-plugin-core/src/post-build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HEADERS } from '@tanstack/start-server-core'
import { HEADERS } from '@tanstack/start-server-core/constants'
import { buildSitemap } from './build-sitemap'
import type { TanStackStartOutputConfig } from './schema'

Expand Down
2 changes: 1 addition & 1 deletion packages/start-plugin-core/src/rsbuild/virtual-modules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core/virtual-modules'
import { generateSerializationAdaptersModule } from '../serialization-adapters-module'
import { generateServerFnResolverModule } from '../start-compiler/server-fn-resolver-module'
import { buildStartManifest } from '../start-manifest-plugin/manifestBuilder'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core/virtual-modules'
import { generateSerializationAdaptersModule } from '../serialization-adapters-module'
import { START_ENVIRONMENT_NAMES } from '../constants'
import { createIdFilter } from '../utils'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core/virtual-modules'
import { resolve as resolvePath } from 'pathe'
import {
SERVER_FN_LOOKUP,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { joinURL } from 'ufo'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core/virtual-modules'
import { rootRouteId } from '@tanstack/router-core'
import { DEV_CLIENT_ENTRY, START_ENVIRONMENT_NAMES } from '../../constants'
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/start-plugin-core/tests/post-server-build.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('@tanstack/start-server-core', () => ({
vi.mock('@tanstack/start-server-core/constants', () => ({
HEADERS: {
TSS_SHELL: 'x-tss-shell',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'pathe'

vi.mock('@tanstack/start-server-core', () => ({
vi.mock('@tanstack/start-server-core/constants', () => ({
HEADERS: {
TSS_SHELL: 'x-tss-shell',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { VIRTUAL_MODULES } from '@tanstack/start-server-core'
import { VIRTUAL_MODULES } from '@tanstack/start-server-core/virtual-modules'
import { describe, expect, test, vi } from 'vitest'
import { DEV_CLIENT_ENTRY, START_ENVIRONMENT_NAMES } from '../src/constants'
import { startManifestPlugin } from '../src/vite/start-manifest-plugin/plugin'

vi.mock('@tanstack/start-server-core', () => ({
vi.mock('@tanstack/start-server-core/virtual-modules', () => ({
VIRTUAL_MODULES: {
startManifest: 'tanstack-start-manifest:v',
},
Expand Down
Loading
Loading