Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions e2e/react-start/flamegraph-bench/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Flamegraph profiles
**/cpu-profile-*.md
**/cpu-profile-*.html
**/cpu-profile-*.pb
**/heap-profile-*.html
**/heap-profile-*.pb
**/heap-profile-*.md
28 changes: 28 additions & 0 deletions e2e/react-start/flamegraph-bench/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "tanstack-react-start-flamegraph-bench",
"private": true,
"type": "module",
"scripts": {
"build": "NODE_ENV=production vite build --mode=production",
"start": "NODE_ENV=production node ./tests/server.js",
"start:prof": "NODE_ENV=production flame run --md-format=detailed ./tests/server.js",
"bench": "pnpm build && pnpm bench:run",
"bench:run": "concurrently -k -s first \"pnpm start:prof\" \"sleep 3 && node ./tests/bench.js\""
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"express": "^4.21.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"srvx": "^0.10.0"
},
"devDependencies": {
"@platformatic/flame": "latest",
"@vitejs/plugin-react": "^4.3.4",
"autocannon": "^8.0.0",
"concurrently": "9.2.1",
"typescript": "^5.7.2",
"vite": "^7.3.1"
}
}
8 changes: 8 additions & 0 deletions e2e/react-start/flamegraph-bench/src/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/react-start/client'
import { getRouter } from './router'

const router = getRouter()

hydrateRoot(document, <StartClient router={router} />)
86 changes: 86 additions & 0 deletions e2e/react-start/flamegraph-bench/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PageIdRouteImport } from './routes/page.$id'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PageIdRoute = PageIdRouteImport.update({
id: '/page/$id',
path: '/page/$id',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/page/$id': typeof PageIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/page/$id': typeof PageIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/page/$id': typeof PageIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/page/$id'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/page/$id'
id: '__root__' | '/' | '/page/$id'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
PageIdRoute: typeof PageIdRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/page/$id': {
id: '/page/$id'
path: '/page/$id'
fullPath: '/page/$id'
preLoaderRoute: typeof PageIdRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
PageIdRoute: PageIdRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

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>>
}
}
11 changes: 11 additions & 0 deletions e2e/react-start/flamegraph-bench/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
})

return router
}
28 changes: 28 additions & 0 deletions e2e/react-start/flamegraph-bench/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react'
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'

export const Route = createRootRoute({
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Flamegraph Benchmark</title>
<HeadContent />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
)
}
26 changes: 26 additions & 0 deletions e2e/react-start/flamegraph-bench/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createFileRoute, Link } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
component: IndexComponent,
})

function IndexComponent() {
return (
<div>
<h1 data-testid="page-title">Flamegraph Benchmark - Index</h1>
<p>Navigate to any page to start:</p>
<div>
{Array.from({ length: 10 }, (_, i) => (
<Link
key={i}
to="/page/$id"
params={{ id: String(i) }}
style={{ marginRight: '10px' }}
>
Page {i}
</Link>
))}
</div>
</div>
)
}
29 changes: 29 additions & 0 deletions e2e/react-start/flamegraph-bench/src/routes/page.$id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createFileRoute, Link } from '@tanstack/react-router'

export const Route = createFileRoute('/page/$id')({
component: PageComponent,
})

function PageComponent() {
const { id } = Route.useParams()

// 100 links = 100 buildLocation calls during SSR
return (
<div>
<h1 data-testid="page-title">Page {id}</h1>
<div>
{Array.from({ length: 100 }, (_, i) => (
<Link
key={i}
to="/page/$id"
params={{ id: String(i % 10) }}
data-testid={`link-${i}`}
style={{ display: 'block', margin: '2px 0' }}
>
Go to Page {i % 10}
</Link>
))}
</div>
</div>
)
}
7 changes: 7 additions & 0 deletions e2e/react-start/flamegraph-bench/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import handler from '@tanstack/react-start/server-entry'

export default {
fetch(request: Request) {
return handler.fetch(request)
},
}
39 changes: 39 additions & 0 deletions e2e/react-start/flamegraph-bench/tests/bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import autocannon from 'autocannon'

const BASE_URL = 'http://localhost:3000'

const instance = autocannon({
url: BASE_URL,
connections: 100, // concurrent connections
duration: 30, // seconds
pipelining: 1, // requests per connection
requests: [
{
setupRequest: (req) => {
// Pick a random page for each request
const randomPage = '/page/' + Math.floor(Math.random() * 1000)
return { ...req, path: randomPage }
},
},
],
})

autocannon.track(instance, { renderProgressBar: true })

instance.on('done', (results) => {
console.log('\n=== SSR Benchmark Results ===')
console.log(`Total requests: ${results.requests.total}`)
console.log(`Requests/sec: ${results.requests.average}`)
console.log(`Latency (avg): ${results.latency.average}ms`)
console.log(`Latency (p99): ${results.latency.p99}ms`)
console.log(
`Throughput: ${(results.throughput.average / 1024 / 1024).toFixed(2)} MB/s`,
)

if (results.errors) {
console.log(`Errors: ${results.errors}`)
}

// Exit after a short delay to allow the server profiler to finish
setTimeout(() => process.exit(0), 1000)
})
30 changes: 30 additions & 0 deletions e2e/react-start/flamegraph-bench/tests/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { toNodeHandler } from 'srvx/node'
import express from 'express'

const port = process.env.PORT || 3000

async function createStartServer() {
const server = (await import('../dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)

const app = express()

// Serve static client assets (matching basic example pattern)
app.use(express.static('../dist/client', { redirect: false }))
Comment on lines +4 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and read the server.js file
find . -name "server.js" -path "*/flamegraph-bench/tests/*" -type f

Repository: TanStack/router

Length of output: 109


🏁 Script executed:

# Check the file contents
cat -n e2e/react-start/flamegraph-bench/tests/server.js

Repository: TanStack/router

Length of output: 994


🏁 Script executed:

# Check the module type of the file (is it ESM or CommonJS?)
head -20 e2e/react-start/flamegraph-bench/tests/server.js

Repository: TanStack/router

Length of output: 610


🏁 Script executed:

# Check package.json to understand the module context
find e2e/react-start/flamegraph-bench -name "package.json" -type f | head -1 | xargs cat

Repository: TanStack/router

Length of output: 954


🌐 Web query:

express.static relative path resolution behavior

💡 Result:

Short answer: express.static resolves a relative root path relative to the current working directory (process.cwd()), i.e. where you launched node — not relative to the module file. Use an absolute path (e.g. path.join(__dirname, 'public')) so resolution is deterministic. [1][2]

Details (one line each):

  • express.static(root, ...) treats the root argument as the directory to serve; a string path is interpreted relative to the directory where the node process was started. [1]
  • Because require() and __dirname are module-relative but process.cwd() depends on how you start the app, using path.join(__dirname, '...') or path.resolve(...) is the recommended pattern. [1][2]

Example:

  • app.use('/static', express.static(path.join(__dirname, 'public'))). [1][2]

Sources:
[1] Express docs — Serving static files.
[2] StackOverflow explanation showing express.static uses the process launch directory.


🌐 Web query:

Node.js process.env.PORT string named pipe behavior

💡 Result:

Short answer: If process.env.PORT is a string that looks like a path (on Windows: begins with \?\pipe\ or \.\pipe), Node's server.listen treats it as an IPC path (named pipe) instead of a TCP port. A numeric string ("3000") is treated as a port number, but a pipe-style string is treated as an IPC path — so pass a Number (e.g. Number(process.env.PORT) or parseInt) when you mean a TCP port, or pass the pipe string when you want a named pipe. [1][2]

Example:

  • TCP port (ensure numeric):
    const port = Number(process.env.PORT) || 3000;
    server.listen(port);
  • Named pipe (Windows):
    const pipe = process.env.PORT; // e.g. "\\?\pipe\myapp"
    server.listen(pipe);

References:

  • Node.js net.server.listen (IPC / named-pipe behavior on Windows). [1]
  • Node.js docs for server.listen variations (port vs path). [2]

🏁 Script executed:

# Check the vite config to understand build output
find e2e/react-start/flamegraph-bench -name "vite.config.*" -o -name "tsconfig.*" | head -5

Repository: TanStack/router

Length of output: 153


🏁 Script executed:

# Look for build configuration and output pattern
find e2e/react-start/flamegraph-bench -type f \( -name "*.config.*" -o -name "*.json" \) | grep -E "(vite|build|tsconfig)" | head -10

Repository: TanStack/router

Length of output: 153


🏁 Script executed:

# Check what files exist in the flamegraph-bench directory structure
ls -la e2e/react-start/flamegraph-bench/ 2>/dev/null | head -20

Repository: TanStack/router

Length of output: 527


🏁 Script executed:

# Verify if there's a dist or build directory already
find e2e/react-start/flamegraph-bench -maxdepth 2 -type d -name "dist" -o -type d -name "build" 2>/dev/null

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check the vite config to understand where dist is built
cat e2e/react-start/flamegraph-bench/vite.config.ts

Repository: TanStack/router

Length of output: 315


🏁 Script executed:

# Also check if there's a root-level vite config
cat e2e/react-start/vite.config.ts 2>/dev/null || cat vite.config.ts 2>/dev/null || echo "No root vite config found"

Repository: TanStack/router

Length of output: 84


🏁 Script executed:

# Check the actual build output by looking at how tanstackStart plugin configures output
# Let's search for dist references in the codebase to understand build structure
rg "dist/client|dist/server" e2e/react-start/flamegraph-bench/ --type ts --type js -A 2 -B 2

Repository: TanStack/router

Length of output: 878


🏁 Script executed:

# Check if there are any other references to how the dist is structured in the project
rg "import.*dist" e2e/react-start/flamegraph-bench/ --type ts --type js

Repository: TanStack/router

Length of output: 175


🏁 Script executed:

# Let's trace the actual build setup by checking package.json build command
cat e2e/react-start/flamegraph-bench/package.json | grep -A 10 "scripts"

Repository: TanStack/router

Length of output: 573


🏁 Script executed:

# Check if there's a monorepo root vite config or build setup
cat vite.config.ts 2>/dev/null | head -30

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Let's see what the actual directory structure shows - does a dist exist anywhere?
find e2e/react-start -maxdepth 3 -type d | grep -E "^e2e/react-start/(dist|flamegraph-bench)" | sort

Repository: TanStack/router

Length of output: 211


🏁 Script executed:

# Check if the coding guidelines mention ESLint router rules for this file
cat .eslintrc* 2>/dev/null | grep -i router || echo "No ESLint config found"

Repository: TanStack/router

Length of output: 81


🏁 Script executed:

# Check if there are ESLint rules configured anywhere
find . -name ".eslintrc*" -o -name "eslint.config.*" | head -5 | xargs cat 2>/dev/null

Repository: TanStack/router

Length of output: 4371


Coerce PORT to number and resolve static asset path relative to module location.

The code passes process.env.PORT as a string to server.listen(), which Node treats as an IPC pipe path on Windows instead of a TCP port. Additionally, express.static('../dist/client') resolves relative to the process working directory, so it points to the wrong location when the build output is in ./dist/client. Coerce PORT to a number and resolve the asset path relative to this file.

Proposed fix
 import { toNodeHandler } from 'srvx/node'
 import express from 'express'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'

-const port = process.env.PORT || 3000
+const port = Number(process.env.PORT) || 3000
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const clientDir = path.resolve(__dirname, '../dist/client')

   // Serve static client assets (matching basic example pattern)
-  app.use(express.static('../dist/client', { redirect: false }))
+  app.use(express.static(clientDir, { redirect: false }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const port = process.env.PORT || 3000
async function createStartServer() {
const server = (await import('../dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)
const app = express()
// Serve static client assets (matching basic example pattern)
app.use(express.static('../dist/client', { redirect: false }))
import { toNodeHandler } from 'srvx/node'
import express from 'express'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const port = Number(process.env.PORT) || 3000
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const clientDir = path.resolve(__dirname, '../dist/client')
async function createStartServer() {
const server = (await import('../dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)
const app = express()
// Serve static client assets (matching basic example pattern)
app.use(express.static(clientDir, { redirect: false }))
🤖 Prompt for AI Agents
In `@e2e/react-start/flamegraph-bench/tests/server.js` around lines 4 - 13, The
port and static path handling in createStartServer are incorrect: coerce
process.env.PORT to a number before using it (the port variable / server.listen)
and resolve the static asset path relative to this module instead of the CWD for
express.static; update the port assignment to parseInt or
Number(process.env.PORT || '3000') and change express.static('../dist/client',
...) to use path.resolve(path.dirname(fileURLToPath(import.meta.url)),
'../dist/client') (referencing createStartServer, port, and the express.static
call) so the server listens on a TCP port and serves the correct build assets.


app.use(async (req, res, next) => {
try {
await nodeHandler(req, res)
} catch (error) {
next(error)
}
})

return { app }
}

createStartServer().then(async ({ app }) =>
app.listen(port, () => {
console.log(`Flamegraph bench server: http://localhost:${port}`)
}),
)
19 changes: 19 additions & 0 deletions e2e/react-start/flamegraph-bench/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "tests"]
}
10 changes: 10 additions & 0 deletions e2e/react-start/flamegraph-bench/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
server: {
port: 3000,
},
plugins: [tanstackStart(), viteReact()],
})
Loading
Loading