-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
docs: add Bun production server guide for TanStack Start #5237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docs: add Bun production server guide for TanStack Start #5237
Conversation
WalkthroughAdds a new React + Bun example with project scaffolding, editor/lint/format configs, routes and generated route tree, Tailwind styles, a Bun production server for hybrid static asset serving, docs updated to prefer Bun, and many Vite devDependency bumps across examples. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Env as Environment
participant BunSrv as Bun Prod Server
participant FS as Filesystem
participant Start as TanStack Start Handler
participant Client as HTTP Client
Env->>BunSrv: read config (PORT, PRELOAD_MAX_BYTES, INCLUDE/EXCLUDE, ETAG, GZIP, VERBOSE)
BunSrv->>FS: scan clientDir, stat files
BunSrv->>BunSrv: build include/exclude regex, classify files (preload vs on-demand)
alt Preload eligible
BunSrv->>BunSrv: read file into memory (opt: gzip, compute ETag)
else Serve on-demand
note right of BunSrv: file served from disk on request
end
BunSrv->>Start: dynamic import of Start handler (server entry)
BunSrv->>BunSrv: mount routes (preloaded assets + fallback to Start)
Client->>BunSrv: request /path
alt Matches preloaded asset
BunSrv-->>Client: respond from memory with headers (Content-Type, Cache, ETag, gzip)
else Matches on-demand asset
BunSrv->>FS: stream file -> BunSrv
BunSrv-->>Client: stream response with headers
else Fallback
BunSrv->>Start: handler.fetch(req)
Start-->>BunSrv: response
BunSrv-->>Client: response
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
docs/start/framework/react/hosting.md
(2 hunks)docs/start/framework/solid/hosting.md
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Files:
docs/start/framework/react/hosting.md
docs/start/framework/solid/hosting.md
docs/{router,start}/**
📄 CodeRabbit inference engine (AGENTS.md)
Place router docs under docs/router/ and start framework docs under docs/start/
Files:
docs/start/framework/react/hosting.md
docs/start/framework/solid/hosting.md
🔇 Additional comments (1)
docs/start/framework/react/hosting.md (1)
221-299
: Bun production server guidance looks solidThe new section clearly walks through setup, configuration, and observability for the Bun server, which should make deployments much smoother.
I had issues with the svrx method (was for some reason extremely slow in a kubernetes pod?) but this one appears to be working very well! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (16)
examples/react/start-bun/src/components/Header.tsx (1)
6-19
: Add nav landmark label for accessibilityAdd an aria-label (or wrap links in a list) to improve screen-reader navigation.
- <nav className="flex flex-row"> + <nav className="flex flex-row" aria-label="Main">docs/start/framework/react/hosting.md (3)
236-246
: Linking outside docs/ likely breaks on the website buildRelative path to examples may not resolve in the docs site. Prefer a GitHub permalink.
As per coding guidelines
Example (adjust branch/commit as appropriate):
-1. Copy the [`server.ts`](../../../../examples/react/start-bun/server.ts) file from the example in this repository to your project root +1. Copy the [`server.ts`](https://github.com/TanStack/router/blob/main/examples/react/start-bun/server.ts) file from the example in this repository to your project root
247-249
: Optional: suggest production script in package.jsonConsider adding a start script in docs to mirror usage: "start": "bun run server.ts".
255-271
: Clarify boolean env parsingIf the server expects a string "true", call it out explicitly to avoid confusion with truthy values.
-# Debug mode with verbose logging -STATIC_PRELOAD_VERBOSE=true bun run server.ts +# Debug mode with verbose logging (set exactly to "true") +STATIC_PRELOAD_VERBOSE="true" bun run server.tsexamples/react/start-bun/package.json (2)
15-27
: Use workspace: for internal TanStack deps in the monorepo*Aligns with repo policy and avoids version drift in examples.
As per coding guidelines
- "@tanstack/react-devtools": "^0.7.0", - "@tanstack/react-router": "^1.132.7", - "@tanstack/react-router-devtools": "^1.132.7", - "@tanstack/react-router-ssr-query": "^1.132.7", - "@tanstack/react-start": "^1.132.7", - "@tanstack/router-plugin": "^1.132.7", + "@tanstack/react-devtools": "workspace:*", + "@tanstack/react-router": "workspace:*", + "@tanstack/react-router-devtools": "workspace:*", + "@tanstack/react-router-ssr-query": "workspace:*", + "@tanstack/react-start": "workspace:*", + "@tanstack/router-plugin": "workspace:*",
5-14
: Fix no-op scripts for lint/formatAs written, "eslint" and "prettier" without args do nothing. Point them at the project.
- "lint": "eslint", - "format": "prettier", - "check": "prettier --write . && eslint --fix" + "lint": "eslint .", + "format": "prettier --write .", + "check": "prettier --check . && eslint ."examples/react/start-bun/src/routes/api.demo-names.ts (1)
6-13
: Prefer Response.json helper and immutable headersSlightly cleaner and sets JSON headers automatically in many runtimes.
- GET: () => { - return new Response(JSON.stringify(['Alice', 'Bob', 'Charlie']), { - headers: { - 'Content-Type': 'application/json', - }, - }) - }, + GET: () => + Response.json(['Alice', 'Bob', 'Charlie']),examples/react/start-bun/src/routes/demo.start.api-request.tsx (2)
5-7
: Handle fetch failures and type the responseAdd a basic ok-check to avoid unhandled rejections and keep types tight.
function getNames() { - return fetch('/api/demo-names').then((res) => res.json()) + return fetch('/api/demo-names').then(async (res) => { + if (!res.ok) throw new Error(`Failed to fetch names: ${res.status}`) + return (await res.json()) as string[] + }) }
16-18
: Optional: surface errors in UI/devtoolsA small catch prevents noisy console errors in SSR/hydration races.
useEffect(() => { - getNames().then(setNames) + getNames().then(setNames).catch((e) => { + console.error(e) + }) }, [])examples/react/start-bun/src/routes/__root.tsx (1)
43-53
: Gate devtools to dev buildsAvoid shipping devtools in production.
- <TanStackDevtools - config={{ - position: 'bottom-left', - }} - plugins={[ - { - name: 'Tanstack Router', - render: <TanStackRouterDevtoolsPanel />, - }, - ]} - /> + {import.meta.env.DEV && ( + <TanStackDevtools + config={{ position: 'bottom-left' }} + plugins={[{ name: 'Tanstack Router', render: <TanStackRouterDevtoolsPanel /> }]} + /> + )}examples/react/start-bun/tsconfig.json (2)
7-8
: Fix included Vite config filenameProject uses
vite.config.ts
, but the include listsvite.config.js
.- "vite.config.js" + "vite.config.ts"
20-21
: Prefer verbatimModuleSyntax with bundlersSetting this to true avoids TS emitting/rewriting import/export syntax and plays nicer with Vite/Bun tree-shaking.
- "verbatimModuleSyntax": false, + "verbatimModuleSyntax": true,examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (2)
27-34
: Add server-side validation and write lazilyValidate input on the server, and import FS within the handler.
-const addTodo = createServerFn({ method: 'POST' }) - .inputValidator((d: string) => d) +const addTodo = createServerFn({ method: 'POST' }) + .inputValidator((d: unknown) => { + if (typeof d !== 'string') throw new Error('Invalid input') + const s = d.trim() + if (!s) throw new Error('Todo cannot be empty') + if (s.length > 200) throw new Error('Todo too long') + return s + }) .handler(async ({ data }) => { const todos = await readTodos() - todos.push({ id: todos.length + 1, name: data }) - await fs.promises.writeFile(filePath, JSON.stringify(todos, null, 2)) + todos.push({ id: todos.length + 1, name: data }) + const { writeFile } = await import('node:fs/promises') + await writeFile(filePath, JSON.stringify(todos, null, 2)) return todos })
47-52
: Tighten useCallback deps
addTodo
is a module constant; no need to include it in deps.- }, [addTodo, todo]) + }, [todo])examples/react/start-bun/server.ts (2)
50-53
: Resolve build paths relative to the server fileUsing relative paths tied to CWD is fragile. Resolve from the module URL.
-import { join } from 'node:path' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' @@ -const PORT = Number(process.env.PORT ?? 3000) -const CLIENT_DIR = './dist/client' -const SERVER_ENTRY = './dist/server/server.js' +const PORT = Number(process.env.PORT ?? 3000) +const ROOT_DIR = fileURLToPath(new URL('.', import.meta.url)) +const CLIENT_DIR = join(ROOT_DIR, 'dist', 'client') +const SERVER_ENTRY = join(ROOT_DIR, 'dist', 'server', 'server.js')
209-211
: Check error.code instead of name for EISDIRSystem errors expose
code
(e.g., 'EISDIR'), not vianame
.- if (error instanceof Error && error.name !== 'EISDIR') { - console.error(`❌ Failed to load ${filepath}:`, error) - } + if (error instanceof Error) { + const code = (error as { code?: string }).code + if (code !== 'EISDIR') { + console.error(`❌ Failed to load ${filepath}:`, error) + } + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (5)
examples/react/start-bun/bun.lock
is excluded by!**/*.lock
examples/react/start-bun/public/favicon.ico
is excluded by!**/*.ico
examples/react/start-bun/public/logo192.png
is excluded by!**/*.png
examples/react/start-bun/public/logo512.png
is excluded by!**/*.png
examples/react/start-bun/src/logo.svg
is excluded by!**/*.svg
📒 Files selected for processing (22)
docs/start/framework/react/hosting.md
(2 hunks)examples/react/start-bun/.gitignore
(1 hunks)examples/react/start-bun/.prettierignore
(1 hunks)examples/react/start-bun/.vscode/settings.json
(1 hunks)examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/eslint.config.js
(1 hunks)examples/react/start-bun/package.json
(1 hunks)examples/react/start-bun/prettier.config.js
(1 hunks)examples/react/start-bun/public/manifest.json
(1 hunks)examples/react/start-bun/public/robots.txt
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)examples/react/start-bun/src/components/Header.tsx
(1 hunks)examples/react/start-bun/src/routeTree.gen.ts
(1 hunks)examples/react/start-bun/src/router.tsx
(1 hunks)examples/react/start-bun/src/routes/__root.tsx
(1 hunks)examples/react/start-bun/src/routes/api.demo-names.ts
(1 hunks)examples/react/start-bun/src/routes/demo.start.api-request.tsx
(1 hunks)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
(1 hunks)examples/react/start-bun/src/routes/index.tsx
(1 hunks)examples/react/start-bun/src/styles.css
(1 hunks)examples/react/start-bun/tsconfig.json
(1 hunks)examples/react/start-bun/vite.config.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (6)
- examples/react/start-bun/public/robots.txt
- examples/react/start-bun/src/styles.css
- examples/react/start-bun/public/manifest.json
- examples/react/start-bun/README.md
- examples/react/start-bun/.prettierignore
- examples/react/start-bun/.gitignore
🧰 Additional context used
📓 Path-based instructions (6)
**/package.json
📄 CodeRabbit inference engine (AGENTS.md)
Use workspace:* protocol for internal dependencies in package.json files
Files:
examples/react/start-bun/package.json
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/package.json
examples/react/start-bun/tsconfig.json
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/prettier.config.js
examples/react/start-bun/eslint.config.js
examples/react/start-bun/src/routeTree.gen.ts
examples/react/start-bun/vite.config.ts
examples/react/start-bun/src/router.tsx
examples/react/start-bun/server.ts
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/components/Header.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/src/routeTree.gen.ts
examples/react/start-bun/vite.config.ts
examples/react/start-bun/src/router.tsx
examples/react/start-bun/server.ts
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/components/Header.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
**/src/routes/**
📄 CodeRabbit inference engine (AGENTS.md)
Place file-based routes under src/routes/ directories
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Files:
docs/start/framework/react/hosting.md
docs/{router,start}/**
📄 CodeRabbit inference engine (AGENTS.md)
Place router docs under docs/router/ and start framework docs under docs/start/
Files:
docs/start/framework/react/hosting.md
🧠 Learnings (1)
📚 Learning: 2025-09-23T17:36:12.598Z
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript in strict mode with extensive type safety across the codebase
Applied to files:
examples/react/start-bun/tsconfig.json
examples/react/start-bun/prettier.config.js
🧬 Code graph analysis (6)
examples/react/start-bun/src/routes/__root.tsx (2)
examples/react/start-bun/src/components/Header.tsx (1)
Header
(3-21)packages/react-router-devtools/src/TanStackRouterDevtoolsPanel.tsx (1)
TanStackRouterDevtoolsPanel
(37-87)
examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
examples/react/start-bun/src/routes/api.demo-names.ts (1)
Route
(3-15)
examples/react/start-bun/src/routeTree.gen.ts (1)
examples/react/start-bun/src/router.tsx (1)
getRouter
(7-13)
examples/react/start-bun/src/routes/index.tsx (4)
examples/react/start-bun/src/routes/__root.tsx (1)
Route
(9-32)examples/react/start-bun/src/routes/api.demo-names.ts (1)
Route
(3-15)examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (1)
Route
(36-39)
examples/react/start-bun/src/routes/api.demo-names.ts (3)
examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (1)
Route
(36-39)examples/react/start-bun/src/routes/index.tsx (1)
Route
(4-6)
examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (2)
examples/react/start-bun/src/routes/api.demo-names.ts (1)
Route
(3-15)examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 84-84: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escaped}$
, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (10)
examples/react/start-bun/.vscode/settings.json (1)
2-10
: LGTM: generated routeTree excluded and readonly is appropriateGood DX guardrails for generated file.
examples/react/start-bun/prettier.config.js (1)
3-10
: LGTM: valid Prettier ESM config with JSDoc typingNo issues.
examples/react/start-bun/eslint.config.js (1)
3-5
: LGTM: Flat config export of TanStack ESLint presetLooks good for ESLint flat config.
examples/react/start-bun/src/routes/index.tsx (2)
4-6
: LGTM: route registration for “/”Matches TanStack Router file-route conventions.
12-27
: LGTM: starter UIAsset import and JSX look correct for Vite + React 19.
examples/react/start-bun/src/router.tsx (1)
7-13
: Router factory looks goodClean router creation with routeTree and sensible defaults.
examples/react/start-bun/vite.config.ts (1)
7-17
: Vite plugin stack LGTMOrder is sensible (paths -> tailwind -> tanstackStart -> react). No issues spotted.
examples/react/start-bun/src/routes/__root.tsx (1)
19-21
: Verify title placement in head()Confirm that
title
inside themeta
array is supported by the Start head API. If not, move it to the top-leveltitle
field of the head config.examples/react/start-bun/server.ts (1)
80-86
: Regex from env input: acceptable here but keep it simpleYou escape everything except '*', which limits ReDoS risk. If you ever expand glob features, re-evaluate this.
Based on static_analysis_hints
examples/react/start-bun/src/routeTree.gen.ts (1)
118-135
: Generated route tree looks consistentThe generated child bindings and module augmentations line up with the referenced routes and the
getRouter
contract. No issues spotted.
import fs from 'node:fs' | ||
import { useCallback, useState } from 'react' | ||
import { createFileRoute, useRouter } from '@tanstack/react-router' | ||
import { createServerFn } from '@tanstack/react-start' | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move Node-only imports to server-only code paths
Top-level node:fs
imports can break the client bundle. Load FS only inside server functions.
-import fs from 'node:fs'
+// Node-only modules must be loaded inside server functions to avoid client bundling
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In examples/react/start-bun/src/routes/demo.start.server-funcs.tsx around lines
1 to 5, the top-level import of node:fs is server-only and will break client
bundles; remove the top-level `import fs from 'node:fs'` and instead import or
require fs inside the server-only function(s) (e.g., use await import('node:fs')
or require('node:fs') within the createServerFn handler) so fs is only loaded on
the server execution path, and keep all other React/client imports at top-level.
async function readTodos() { | ||
return JSON.parse( | ||
await fs.promises.readFile(filePath, 'utf-8').catch(() => | ||
JSON.stringify( | ||
[ | ||
{ id: 1, name: 'Get groceries' }, | ||
{ id: 2, name: 'Buy a new phone' }, | ||
], | ||
null, | ||
2, | ||
), | ||
), | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Load FS lazily inside the server helper
Avoid referencing Node modules at module scope.
-async function readTodos() {
- return JSON.parse(
- await fs.promises.readFile(filePath, 'utf-8').catch(() =>
- JSON.stringify(
- [
- { id: 1, name: 'Get groceries' },
- { id: 2, name: 'Buy a new phone' },
- ],
- null,
- 2,
- ),
- ),
- )
-}
+async function readTodos() {
+ try {
+ const { readFile } = await import('node:fs/promises')
+ return JSON.parse(await readFile(filePath, 'utf-8'))
+ } catch {
+ return [
+ { id: 1, name: 'Get groceries' },
+ { id: 2, name: 'Buy a new phone' },
+ ]
+ }
+}
📝 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.
async function readTodos() { | |
return JSON.parse( | |
await fs.promises.readFile(filePath, 'utf-8').catch(() => | |
JSON.stringify( | |
[ | |
{ id: 1, name: 'Get groceries' }, | |
{ id: 2, name: 'Buy a new phone' }, | |
], | |
null, | |
2, | |
), | |
), | |
) | |
} | |
async function readTodos() { | |
try { | |
const { readFile } = await import('node:fs/promises') | |
return JSON.parse(await readFile(filePath, 'utf-8')) | |
} catch { | |
return [ | |
{ id: 1, name: 'Get groceries' }, | |
{ id: 2, name: 'Buy a new phone' }, | |
] | |
} | |
} |
🤖 Prompt for AI Agents
In examples/react/start-bun/src/routes/demo.start.server-funcs.tsx around lines
8 to 21, the helper reads the filesystem at module scope which references Node
modules too early; move the fs access into the async function to load FS lazily
(use a dynamic import or require inside readTodos), then use the imported
fs.promises.readFile call and keep the same fallback JSON string on read
failure; ensure filePath is still available or passed in, and handle errors
locally so no Node builtin is referenced during module initialization.
Based on this, here's a version including explicit etag generation + gzipping. I'll let you add documentation as needed (or reorganize things) /**
* TanStack Start Production Server with Bun
*
* A high-performance production server for TanStack Start applications that
* implements intelligent static asset loading with configurable memory management.
*
* Features:
* - Hybrid loading strategy (preload small files, serve large files on-demand)
* - Configurable file filtering with include/exclude patterns
* - Memory-efficient response generation
* - Production-ready caching headers
*
* Environment Variables:
*
* PORT (number)
* - Server port number
* - Default: 3000
*
* STATIC_PRELOAD_MAX_BYTES (number)
* - Maximum file size in bytes to preload into memory
* - Files larger than this will be served on-demand from disk
* - Default: 5242880 (5MB)
* - Example: STATIC_PRELOAD_MAX_BYTES=5242880 (5MB)
*
* STATIC_PRELOAD_INCLUDE (string)
* - Comma-separated list of glob patterns for files to include
* - If specified, only matching files are eligible for preloading
* - Patterns are matched against filenames only, not full paths
* - Example: STATIC_PRELOAD_INCLUDE="*.js,*.css,*.woff2"
*
* STATIC_PRELOAD_EXCLUDE (string)
* - Comma-separated list of glob patterns for files to exclude
* - Applied after include patterns
* - Patterns are matched against filenames only, not full paths
* - Example: STATIC_PRELOAD_EXCLUDE="*.map,*.txt"
*
* STATIC_PRELOAD_VERBOSE (boolean)
* - Enable detailed logging of loaded and skipped files
* - Default: false
* - Set to "true" to enable verbose output
*
* STATIC_PRELOAD_ETAG (boolean)
* - Enable ETag generation and conditional 304 responses for preloaded assets
* - Default: true
*
* STATIC_PRELOAD_GZIP (boolean)
* - Enable gzip precompression for eligible preloaded assets
* - Default: true
*
* STATIC_PRELOAD_GZIP_MIN_BYTES (number)
* - Minimum size (in bytes) before a file is considered for gzip
* - Default: 1024 (1KB)
*
* STATIC_PRELOAD_GZIP_TYPES (string)
* - Comma-separated list of MIME types or prefixes (ending with /) that can be gzip-precompressed
* - Default: text/,application/javascript,application/json,application/xml,image/svg+xml
*
* Usage:
* bun run server.ts
*/
// Configuration
const PORT = Number(process.env.PORT || '3000')
const CLIENT_DIR = './dist/client'
const SERVER_ENTRY = './dist/server/server.js'
// Preloading configuration from environment variables
const MAX_PRELOAD_BYTES = Number(
process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, // 5MB default
)
// Parse comma-separated include patterns (no defaults)
const INCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_INCLUDE ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map(globToRegExp)
// Parse comma-separated exclude patterns (no defaults)
const EXCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_EXCLUDE ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map(globToRegExp)
// Verbose logging flag
const VERBOSE = process.env.STATIC_PRELOAD_VERBOSE === 'true'
// Optional features
const ENABLE_ETAG = (process.env.STATIC_PRELOAD_ETAG || 'true') === 'true'
const ENABLE_GZIP = (process.env.STATIC_PRELOAD_GZIP || 'true') === 'true'
const GZIP_MIN_BYTES = Number(process.env.STATIC_PRELOAD_GZIP_MIN_BYTES || 1024) // 1KB
const GZIP_TYPES = (
process.env.STATIC_PRELOAD_GZIP_TYPES ||
'text/,application/javascript,application/json,application/xml,image/svg+xml'
)
.split(',')
.map((v) => v.trim())
.filter(Boolean)
/**
* Convert a simple glob pattern to a regular expression
* Supports * wildcard for matching any characters
*/
function globToRegExp(glob: string): RegExp {
// Escape regex special chars except *, then replace * with .*
const escaped = glob
.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&')
.replace(/\*/g, '.*')
return new RegExp(`^${escaped}$`, 'i')
}
/**
* Metadata for preloaded static assets
*/
interface AssetMetadata {
route: string
size: number
type: string
}
/**
* Result of static asset preloading process
*/
interface PreloadResult {
routes: Record<string, (req: Request) => Response | Promise<Response>>
loaded: Array<AssetMetadata>
skipped: Array<AssetMetadata>
}
/**
* Check if a file should be included based on configured patterns
*/
function shouldInclude(relativePath: string): boolean {
const fileName = relativePath.split(/[/\\]/).pop() ?? relativePath
if (INCLUDE_PATTERNS.length > 0) {
if (!INCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
}
if (EXCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
return true
}
function matchesCompressible(type: string) {
return GZIP_TYPES.some((t) =>
t.endsWith('/') ? type.startsWith(t) : type === t,
)
}
interface InMemoryAsset {
raw: Uint8Array
gz?: Uint8Array
etag?: string
type: string
immutable: boolean
size: number
}
function computeEtag(data: Uint8Array): string {
const hash = Bun.hash(data)
return `W/"${hash.toString(16)}-${data.byteLength}"`
}
function buildResponseFactory(
asset: InMemoryAsset,
): (req: Request) => Response {
return (req: Request) => {
const headers: Record<string, string> = {
'Content-Type': asset.type,
'Cache-Control': asset.immutable
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600',
}
if (ENABLE_ETAG && asset.etag) {
const ifNone = req.headers.get('if-none-match')
if (ifNone && ifNone === asset.etag) {
return new Response(null, {
status: 304,
headers: { ETag: asset.etag },
})
}
headers.ETag = asset.etag
}
if (
ENABLE_GZIP &&
asset.gz &&
req.headers.get('accept-encoding')?.includes('gzip')
) {
console.log(`Serving precompressed asset for ${req.url}`)
headers['Content-Encoding'] = 'gzip'
headers['Content-Length'] = String(asset.gz.byteLength)
const gzCopy = new Uint8Array(asset.gz)
return new Response(gzCopy, { status: 200, headers })
}
headers['Content-Length'] = String(asset.raw.byteLength)
const rawCopy = new Uint8Array(asset.raw)
return new Response(rawCopy, { status: 200, headers })
}
}
async function gzipMaybe(
data: Uint8Array<ArrayBuffer>,
type: string,
): Promise<Uint8Array | undefined> {
if (!ENABLE_GZIP) return undefined
if (data.byteLength < GZIP_MIN_BYTES) return undefined
if (!matchesCompressible(type)) return undefined
try {
return Bun.gzipSync(data)
} catch {
return undefined
}
}
function makeOnDemandFactory(filepath: string, type: string) {
return (_req: Request) => {
const f = Bun.file(filepath)
return new Response(f, {
headers: {
'Content-Type': type,
'Cache-Control': 'public, max-age=3600',
},
})
}
}
function buildCompositeGlob(): Bun.Glob {
const raw = (process.env.STATIC_PRELOAD_INCLUDE || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (raw.length === 0) return new Bun.Glob('**/*')
if (raw.length === 1) return new Bun.Glob(raw[0])
return new Bun.Glob(`{${raw.join(',')}}`)
}
/**
* Build static routes with intelligent preloading strategy
* Small files are loaded into memory, large files are served on-demand
*/
async function buildStaticRoutes(clientDir: string): Promise<PreloadResult> {
const routes: Record<string, (req: Request) => Response | Promise<Response>> =
{}
const loaded: Array<AssetMetadata> = []
const skipped: Array<AssetMetadata> = []
console.log(`📦 Loading static assets from ${clientDir}...`)
console.log(
` Max preload size: ${(MAX_PRELOAD_BYTES / 1024 / 1024).toFixed(2)} MB`,
)
if (INCLUDE_PATTERNS.length > 0) {
console.log(
` Include patterns: ${process.env.STATIC_PRELOAD_INCLUDE ?? ''}`,
)
}
if (EXCLUDE_PATTERNS.length > 0) {
console.log(
` Exclude patterns: ${process.env.STATIC_PRELOAD_EXCLUDE ?? ''}`,
)
}
console.log(' ETag generation:', ENABLE_ETAG ? 'enabled' : 'disabled')
console.log(' Gzip precompression:', ENABLE_GZIP ? 'enabled' : 'disabled')
if (ENABLE_GZIP) {
console.log(` Gzip min size: ${(GZIP_MIN_BYTES / 1024).toFixed(2)} kB`)
console.log(` Gzip types: ${GZIP_TYPES.join(', ')}`)
}
let totalPreloadedBytes = 0
const gzSizes: Record<string, number> = {}
try {
const glob = buildCompositeGlob()
for await (const relativePath of glob.scan({ cwd: clientDir })) {
const filepath = `${clientDir}/${relativePath}`
const route = `/${relativePath}`
try {
const file = Bun.file(filepath)
if (!(await file.exists()) || file.size === 0) {
continue
}
const metadata: AssetMetadata = {
route,
size: file.size,
type: file.type || 'application/octet-stream',
}
const matchesPattern = shouldInclude(relativePath)
const withinSizeLimit = file.size <= MAX_PRELOAD_BYTES
if (matchesPattern && withinSizeLimit) {
const bytes = new Uint8Array(await file.arrayBuffer())
const gz = await gzipMaybe(bytes, metadata.type)
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined
const asset: InMemoryAsset = {
raw: bytes,
gz,
etag,
type: metadata.type,
immutable: true,
size: bytes.byteLength,
}
routes[route] = buildResponseFactory(asset)
loaded.push({ ...metadata, size: bytes.byteLength })
totalPreloadedBytes += bytes.byteLength
if (gz) gzSizes[route] = gz.byteLength
} else {
routes[route] = makeOnDemandFactory(filepath, metadata.type)
skipped.push(metadata)
}
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'EISDIR') {
console.error(`❌ Failed to load ${filepath}:`, error)
}
}
}
if (loaded.length > 0 || skipped.length > 0) {
const allFiles = [...loaded, ...skipped].sort((a, b) =>
a.route.localeCompare(b.route),
)
const maxPathLength = Math.min(
Math.max(...allFiles.map((f) => f.route.length)),
60,
)
function formatKB(bytes: number) {
const kb = bytes / 1024
return kb < 100 ? kb.toFixed(2) : kb.toFixed(1)
}
if (loaded.length > 0) {
console.log('\n📁 Preloaded into memory:')
loaded
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const sizeStr = `${formatKB(file.size).padStart(7)} kB`
const paddedPath = file.route.padEnd(maxPathLength)
const gzSize = gzSizes[file.route]
if (gzSize) {
const gzStr = `${formatKB(gzSize).padStart(7)} kB`
console.log(` ${paddedPath} ${sizeStr} │ gzip: ${gzStr}`)
} else {
console.log(` ${paddedPath} ${sizeStr}`)
}
})
}
if (skipped.length > 0) {
console.log('\n💾 Served on-demand:')
skipped
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const sizeStr = `${formatKB(file.size).padStart(7)} kB`
const paddedPath = file.route.padEnd(maxPathLength)
console.log(` ${paddedPath} ${sizeStr}`)
})
}
if (VERBOSE) {
console.log('\n📊 Detailed file information:')
allFiles.forEach((file) => {
const isPreloaded = loaded.includes(file)
const status = isPreloaded ? '[MEMORY]' : '[ON-DEMAND]'
const reason =
!isPreloaded && file.size > MAX_PRELOAD_BYTES
? ' (too large)'
: !isPreloaded
? ' (filtered)'
: ''
console.log(
` ${status.padEnd(12)} ${file.route} - ${file.type}${reason}`,
)
})
}
}
console.log()
if (loaded.length > 0) {
console.log(
`✅ Preloaded ${String(loaded.length)} files (${(totalPreloadedBytes / 1024 / 1024).toFixed(2)} MB) into memory`,
)
} else {
console.log('ℹ️ No files preloaded into memory')
}
if (skipped.length > 0) {
const tooLarge = skipped.filter((f) => f.size > MAX_PRELOAD_BYTES).length
const filtered = skipped.length - tooLarge
console.log(
`ℹ️ ${String(skipped.length)} files will be served on-demand (${String(tooLarge)} too large, ${String(filtered)} filtered)`,
)
}
} catch (error) {
console.error(`❌ Failed to load static files from ${clientDir}:`, error)
}
return { routes, loaded, skipped }
}
/**
* Start the production server
*/
async function startServer() {
console.log('🚀 Starting production server...')
let handler: { fetch: (request: Request) => Response | Promise<Response> }
try {
const serverModule = (await import(SERVER_ENTRY)) as {
default: { fetch: (request: Request) => Response | Promise<Response> }
}
handler = serverModule.default
console.log('✅ TanStack Start handler loaded')
} catch (error) {
console.error('❌ Failed to load server handler:', error)
process.exit(1)
}
const { routes } = await buildStaticRoutes(CLIENT_DIR)
const server = Bun.serve({
port: PORT,
routes: {
...routes,
'/*': (req: Request) => handler.fetch(req),
},
error(error) {
console.error('Uncaught server error:', error)
return new Response('Internal Server Error', { status: 500 })
},
})
console.log(
`\n🚀 Server running at http://localhost:${String(server.port)}\n`,
)
}
await startServer().catch((error: unknown) => {
console.error('Failed to start server:', error)
process.exit(1)
}) |
One thing I'm doing in my own version is serving static files that end with Example: Also I'm doing the same for top-level Example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
examples/react/start-bun/server.ts (6)
228-265
: Add Vary header, HEAD support, and avoid copying buffers in responses
- Add Vary: Accept-Encoding to prevent cache poisoning.
- Handle HEAD requests (headers only, no body).
- Avoid cloning Uint8Array bodies to reduce allocations.
function createResponseHandler( asset: InMemoryAsset, ): (req: Request) => Response { return (req: Request) => { - const headers: Record<string, string> = { + const headers: Record<string, string> = { "Content-Type": asset.type, "Cache-Control": asset.immutable ? "public, max-age=31536000, immutable" : "public, max-age=3600", }; - if (ENABLE_ETAG && asset.etag) { - const ifNone = req.headers.get("if-none-match"); - if (ifNone && ifNone === asset.etag) { - return new Response(null, { - status: 304, - headers: { ETag: asset.etag }, - }); - } - headers.ETag = asset.etag; - } + headers["Vary"] = "Accept-Encoding"; + + if (ENABLE_ETAG && asset.etag) { + const ifNone = req.headers.get("if-none-match"); + if (ifNone && ifNone === asset.etag) { + return new Response(null, { + status: 304, + headers: { ...headers, ETag: asset.etag }, + }); + } + headers.ETag = asset.etag; + } - if ( - ENABLE_GZIP && - asset.gz && - req.headers.get("accept-encoding")?.includes("gzip") - ) { - headers["Content-Encoding"] = "gzip"; - headers["Content-Length"] = String(asset.gz.byteLength); - const gzCopy = new Uint8Array(asset.gz); - return new Response(gzCopy, { status: 200, headers }); - } + const accept = req.headers.get("accept-encoding") ?? ""; + const useGzip = + ENABLE_GZIP && asset.gz && accept.includes("gzip"); + + if (req.method === "HEAD") { + if (useGzip) { + headers["Content-Encoding"] = "gzip"; + headers["Content-Length"] = String(asset.gz!.byteLength); + } else { + headers["Content-Length"] = String(asset.raw.byteLength); + } + return new Response(null, { status: 200, headers }); + } - headers["Content-Length"] = String(asset.raw.byteLength); - const rawCopy = new Uint8Array(asset.raw); - return new Response(rawCopy, { status: 200, headers }); + if (useGzip) { + headers["Content-Encoding"] = "gzip"; + headers["Content-Length"] = String(asset.gz!.byteLength); + return new Response(asset.gz!, { status: 200, headers }); + } + + headers["Content-Length"] = String(asset.raw.byteLength); + return new Response(asset.raw, { status: 200, headers }); }; }
219-221
: Use Uint8Array directly with Bun.gzipSyncPassing data.buffer risks mis-sized output if the view is a slice; passing the Uint8Array is clearer and safe.
- return Bun.gzipSync(data.buffer as ArrayBuffer); + return Bun.gzipSync(data);
270-278
: Normalize include patterns to “filename-only” semanticsDocs say includes match filenames only, but Bun.Glob matches paths. Normalize patterns without slashes to
**/pattern
so “*.js” matches at any depth.function createCompositeGlobPattern(): Bun.Glob { - const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "") + const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "") .split(",") .map((s) => s.trim()) .filter(Boolean); - if (raw.length === 0) return new Bun.Glob("**/*"); - if (raw.length === 1) return new Bun.Glob(raw[0]); - return new Bun.Glob(`{${raw.join(",")}}`); + const normalized = + raw.length === 0 + ? ["**/*"] + : raw.map((p) => (p.includes("/") ? p : `**/${p}`)); + if (normalized.length === 1) return new Bun.Glob(normalized[0]); + return new Bun.Glob(`{${normalized.join(",")}}`); }
313-316
: Only mark hashed assets as immutable (avoid stale caches for non-fingerprinted files)Treat files with a long hex hash in the filename as immutable; use shorter cache for others, including on-demand.
- for await (const relativePath of glob.scan({ cwd: clientDirectory })) { + for await (const relativePath of glob.scan({ cwd: clientDirectory })) { const filepath = `${clientDirectory}/${relativePath}`; const route = `/${relativePath}`; + const fileName = relativePath.split(/[/\\]/).pop()!; @@ - if (matchesPattern && withinSizeLimit) { + if (matchesPattern && withinSizeLimit) { + const isImmutableFile = /[.-][a-f0-9]{8,}\./i.test(fileName); // Preload small files into memory with ETag and Gzip support const bytes = new Uint8Array(await file.arrayBuffer()); const gz = compressDataIfAppropriate(bytes, metadata.type); const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined; const asset: InMemoryAsset = { raw: bytes, gz, etag, type: metadata.type, - immutable: true, + immutable: isImmutableFile, size: bytes.byteLength, }; - routes[route] = createResponseHandler(asset); + const responder = createResponseHandler(asset); + routes[route] = responder; + // Directory index and extensionless HTML aliases + if (route.endsWith("/index.html")) { + routes[route.slice(0, -"index.html".length)] = responder; + } else if (route.endsWith(".html")) { + routes[route.slice(0, -".html".length)] = responder; + } @@ - routes[route] = () => { + const isImmutableFile = /[.-][a-f0-9]{8,}\./i.test(fileName); + const responder = () => { const fileOnDemand = Bun.file(filepath); return new Response(fileOnDemand, { headers: { "Content-Type": metadata.type, - "Cache-Control": "public, max-age=3600", + "Cache-Control": isImmutableFile + ? "public, max-age=31536000, immutable" + : "public, max-age=3600", }, }); - }; + }; + routes[route] = responder; + // Directory index and extensionless HTML aliases + if (route.endsWith("/index.html")) { + routes[route.slice(0, -"index.html".length)] = responder; + } else if (route.endsWith(".html")) { + routes[route.slice(0, -".html".length)] = responder; + }Also applies to: 336-349, 359-361
504-511
: Support both default and named exports for the server ‘fetch’ handlerBe resilient to differing build outputs (default export vs named
fetch
).- try { - const serverModule = (await import(SERVER_ENTRY_POINT)) as { - default: { fetch: (request: Request) => Response | Promise<Response> }; - }; - handler = serverModule.default; + try { + const serverModule: any = await import(SERVER_ENTRY_POINT); + const maybeHandler = serverModule?.default ?? serverModule; + if (typeof maybeHandler?.fetch !== "function") { + throw new Error("Server entry does not export a fetch handler"); + } + handler = maybeHandler as { fetch: (request: Request) => Response | Promise<Response> }; log.success("TanStack Start application handler initialized");
132-138
: Regex from env input flagged (low risk as implemented); consider using Bun.Glob or guardrailsCurrent conversion escapes all special chars and only expands “” to “.”, anchored. This is generally safe, but static analysis flags ReDoS risks on variable regex. Optionally:
- Cap pattern length (e.g., 512 chars) and reject overly long inputs.
- Prefer Bun.Glob for matching names when possible.
Based on static analysis hints
function convertGlobToRegExp(globPattern: string): RegExp { + if (globPattern.length > 512) { + throw new Error("Glob pattern too long"); + } // Escape regex special chars except *, then replace * with .* const escapedPattern = globPattern .replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&") .replace(/\*/g, ".*"); return new RegExp(`^${escapedPattern}$`, "i"); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
examples/react/start-bun/server.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/server.ts
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/server.ts
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 136-136: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, "i")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
Hello, when running |
View your CI Pipeline Execution ↗ for commit 70d0c75
☁️ Nx Cloud last updated this comment at |
81b7a95
to
4f6531e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (5)
examples/react/start-bun/public/manifest.json (1)
21-24
: Prefer explicit PWA scope/id and root start_urlSmall PWA polish: set scope/id and use start_url "/" for consistent root behavior across deployments.
Apply:
- "start_url": ".", + "scope": "/", + "id": "/", + "start_url": "/",examples/react/start-bun/server.ts (1)
132-138
: Consider replacing custom glob-to-regex with Bun.Glob to avoid ReDoS risksThese regexes are built from env-supplied patterns; while escaped and compiled once, using Bun.Glob for include/exclude avoids regex pitfalls and keeps semantics consistent with your scanner.
Based on static analysis hints
docs/start/framework/react/hosting.md (1)
287-293
: Document ETAG/GZIP toggles to match server capabilitiesExpose the remaining configuration the server supports for completeness.
Apply:
- `PORT`: Server port (default: 3000) - `STATIC_PRELOAD_MAX_BYTES`: Maximum file size to preload in bytes (default: 5242880 = 5MB) - `STATIC_PRELOAD_INCLUDE`: Comma-separated glob patterns for files to include - `STATIC_PRELOAD_EXCLUDE`: Comma-separated glob patterns for files to exclude - `STATIC_PRELOAD_VERBOSE`: Enable detailed logging (set to "true") +- `STATIC_PRELOAD_ETAG`: Enable ETag generation for preloaded assets (default: "true") +- `STATIC_PRELOAD_GZIP`: Enable gzip precompression and delivery for eligible assets (default: "true") +- `STATIC_PRELOAD_GZIP_MIN_BYTES`: Minimum size in bytes for gzip (default: 1024) +- `STATIC_PRELOAD_GZIP_TYPES`: Comma-separated MIME types eligible for gzip (default: text/,application/javascript,application/json,application/xml,image/svg+xml)examples/react/start-bun/src/routes/api.demo-names.ts (1)
6-12
: Use Response.json for claritySlightly cleaner and sets the header automatically.
Apply:
- GET: () => { - return new Response(JSON.stringify(['Alice', 'Bob', 'Charlie']), { - headers: { - 'Content-Type': 'application/json', - }, - }) - }, + GET: () => Response.json(['Alice', 'Bob', 'Charlie']),examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
5-7
: Type the helper for better inferenceReturn type annotation improves DX and avoids any.
Apply:
-function getNames() { - return fetch('/api/demo-names').then((res) => res.json()) -} +function getNames(): Promise<string[]> { + return fetch('/api/demo-names').then((res) => res.json()) +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
examples/react/start-bun/bun.lock
is excluded by!**/*.lock
examples/react/start-bun/public/favicon.ico
is excluded by!**/*.ico
examples/react/start-bun/public/logo192.png
is excluded by!**/*.png
examples/react/start-bun/public/logo512.png
is excluded by!**/*.png
examples/react/start-bun/src/logo.svg
is excluded by!**/*.svg
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
docs/start/framework/react/hosting.md
(2 hunks)examples/react/start-bun/.gitignore
(1 hunks)examples/react/start-bun/.prettierignore
(1 hunks)examples/react/start-bun/.vscode/settings.json
(1 hunks)examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/eslint.config.js
(1 hunks)examples/react/start-bun/package.json
(1 hunks)examples/react/start-bun/prettier.config.js
(1 hunks)examples/react/start-bun/public/manifest.json
(1 hunks)examples/react/start-bun/public/robots.txt
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)examples/react/start-bun/src/components/Header.tsx
(1 hunks)examples/react/start-bun/src/routeTree.gen.ts
(1 hunks)examples/react/start-bun/src/router.tsx
(1 hunks)examples/react/start-bun/src/routes/__root.tsx
(1 hunks)examples/react/start-bun/src/routes/api.demo-names.ts
(1 hunks)examples/react/start-bun/src/routes/demo.start.api-request.tsx
(1 hunks)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
(1 hunks)examples/react/start-bun/src/routes/index.tsx
(1 hunks)examples/react/start-bun/src/styles.css
(1 hunks)examples/react/start-bun/tsconfig.json
(1 hunks)examples/react/start-bun/vite.config.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- examples/react/start-bun/.gitignore
🚧 Files skipped from review as they are similar to previous changes (12)
- examples/react/start-bun/.prettierignore
- examples/react/start-bun/src/components/Header.tsx
- examples/react/start-bun/tsconfig.json
- examples/react/start-bun/eslint.config.js
- examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
- examples/react/start-bun/.vscode/settings.json
- examples/react/start-bun/README.md
- examples/react/start-bun/src/router.tsx
- examples/react/start-bun/vite.config.ts
- examples/react/start-bun/package.json
- examples/react/start-bun/src/routeTree.gen.ts
- examples/react/start-bun/prettier.config.js
🧰 Additional context used
📓 Path-based instructions (5)
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/public/manifest.json
examples/react/start-bun/public/robots.txt
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/src/styles.css
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/server.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/server.ts
**/src/routes/**
📄 CodeRabbit inference engine (AGENTS.md)
Place file-based routes under src/routes/ directories
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/index.tsx
examples/react/start-bun/src/routes/demo.start.api-request.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Files:
docs/start/framework/react/hosting.md
docs/{router,start}/**
📄 CodeRabbit inference engine (AGENTS.md)
Place router docs under docs/router/ and start framework docs under docs/start/
Files:
docs/start/framework/react/hosting.md
🧬 Code graph analysis (4)
examples/react/start-bun/src/routes/__root.tsx (2)
examples/react/start-bun/src/components/Header.tsx (1)
Header
(3-21)packages/react-router-devtools/src/TanStackRouterDevtoolsPanel.tsx (1)
TanStackRouterDevtoolsPanel
(37-87)
examples/react/start-bun/src/routes/index.tsx (4)
examples/react/start-bun/src/routes/__root.tsx (1)
Route
(9-32)examples/react/start-bun/src/routes/api.demo-names.ts (1)
Route
(3-15)examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (1)
Route
(36-39)
examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
examples/react/start-bun/src/routes/api.demo-names.ts (1)
Route
(3-15)
examples/react/start-bun/src/routes/api.demo-names.ts (4)
examples/react/start-bun/src/routes/__root.tsx (1)
Route
(9-32)examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (1)
Route
(36-39)examples/react/start-bun/src/routes/index.tsx (1)
Route
(4-6)
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 136-136: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, "i")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (4)
examples/react/start-bun/src/styles.css (1)
1-15
: Styling defaults look solidBringing Tailwind in at the top and zeroing the body margin while defining the font stacks gives the example sensible defaults. No issues spotted.
docs/start/framework/react/hosting.md (2)
217-218
: Use stable React 19 range and bun addPrefer the stable range and bun add for clarity and alignment with current releases.
Apply:
-```sh -bun install react@19 react-dom@19 +```sh +bun add react@^19 react-dom@^19--- `266-284`: **Align env var names with server.ts** The example uses STATIC_PRELOAD_* while server.ts originally used ASSET_PRELOAD_*. After unifying code to STATIC_PRELOAD_*, this section is correct. Ensure the names below match code exactly. If you keep ASSET_* anywhere, update or add a note for backward compatibility. </blockquote></details> <details> <summary>examples/react/start-bun/public/manifest.json (1)</summary><blockquote> `4-19`: **Manifest icons present** All referenced icons (favicon.ico, logo192.png, logo512.png) exist under examples/react/start-bun/public. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
1. Copy the [`server.ts`](../../../../examples/react/start-bun/server.ts) file from the example in this repository to your project root | ||
|
||
2. Build your application: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid linking to repo files via deep relative paths; use a stable GitHub URL
Deep relative paths outside docs/ often break on the docs site build. Link to the example file on GitHub.
Apply:
-1. Copy the [`server.ts`](../../../../examples/react/start-bun/server.ts) file from the example in this repository to your project root
+1. Copy the `server.ts` file from the example in this repository to your project root:
+ https://github.com/TanStack/router/blob/main/examples/react/start-bun/server.ts
As per coding guidelines
🤖 Prompt for AI Agents
In docs/start/framework/react/hosting.md around lines 248 to 250, replace the
deep relative link to ../../../../examples/react/start-bun/server.ts with a
stable GitHub URL to the example file (use the repository's canonical branch,
e.g.
https://github.com/<org>/<repo>/blob/main/examples/react/start-bun/server.ts or
the raw URL if appropriate for embedding); update the text so it references that
absolute GitHub link and verify the link points to the correct branch and path
so docs builds won't break due to relative path traversal outside docs/.
const MAX_PRELOAD_BYTES = Number( | ||
process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default | ||
); | ||
|
||
// Parse comma-separated include patterns (no defaults) | ||
const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "") | ||
.split(",") | ||
.map((s) => s.trim()) | ||
.filter(Boolean) | ||
.map((pattern: string) => convertGlobToRegExp(pattern)); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Unify env var names with docs (STATIC_PRELOAD_*), avoid drift
Docs and examples use STATIC_PRELOAD_* but code uses ASSET_PRELOAD_*. This will confuse users and break copy-paste setups.
Apply:
-const MAX_PRELOAD_BYTES = Number(
- process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default
-)
+const MAX_PRELOAD_BYTES = Number(
+ process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, // 5MB default
+)
-const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "")
+const INCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_INCLUDE ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern));
-const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? "")
+const EXCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_EXCLUDE ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern));
-const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === "true";
+const VERBOSE = process.env.STATIC_PRELOAD_VERBOSE === "true";
-const ENABLE_ETAG =
- (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? "true") === "true";
+const ENABLE_ETAG = (process.env.STATIC_PRELOAD_ETAG ?? "true") === "true";
-const ENABLE_GZIP =
- (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? "true") === "true";
-const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024); // 1KB
-const GZIP_TYPES = (
- process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ??
+const ENABLE_GZIP = (process.env.STATIC_PRELOAD_GZIP ?? "true") === "true";
+const GZIP_MIN_BYTES = Number(process.env.STATIC_PRELOAD_GZIP_MIN_BYTES ?? 1024); // 1KB
+const GZIP_TYPES = (
+ process.env.STATIC_PRELOAD_GZIP_TYPES ??
"text/,application/javascript,application/json,application/xml,image/svg+xml"
)
function createCompositeGlobPattern(): Bun.Glob {
- const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? "")
+ const raw = (process.env.STATIC_PRELOAD_INCLUDE ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (raw.length === 0) return new Bun.Glob("**/*");
if (raw.length === 1) return new Bun.Glob(raw[0]);
return new Bun.Glob(`{${raw.join(",")}}`);
}
- console.log(`Include patterns: ${process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? ""}`);
+ console.log(`Include patterns: ${process.env.STATIC_PRELOAD_INCLUDE ?? ""}`);
- console.log(`Exclude patterns: ${process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? ""}`);
+ console.log(`Exclude patterns: ${process.env.STATIC_PRELOAD_EXCLUDE ?? ""}`);
Also update the file’s top comment block to reference STATIC_PRELOAD_* for consistency.
Run to ensure no lingering ASSET_PRELOAD_*:
Also applies to: 103-110, 112-127, 270-278, 297-306
🏁 Script executed:
#!/bin/bash
rg -n --no-ignore 'ASSET_PRELOAD_' --type=ts -C1
Length of output: 4750
🏁 Script executed:
#!/bin/bash
# Search for STATIC_PRELOAD_ references across the repository
rg -n 'STATIC_PRELOAD_' -C1
Length of output: 2940
Use STATIC_PRELOAD_ env vars in server.ts to match docs*
Replace all ASSET_PRELOAD_* references (top comment block and code at lines 91–101, 103–110, 112–127, 270–278, 297–306) with the corresponding STATIC_PRELOAD_* names from README.md and hosting.md.
function compressDataIfAppropriate( | ||
data: Uint8Array, | ||
mimeType: string, | ||
): Uint8Array | undefined { | ||
if (!ENABLE_GZIP) return undefined; | ||
if (data.byteLength < GZIP_MIN_BYTES) return undefined; | ||
if (!isMimeTypeCompressible(mimeType)) return undefined; | ||
try { | ||
return Bun.gzipSync(data.buffer as ArrayBuffer); | ||
} catch { | ||
return undefined; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Possible incorrect gzip due to using underlying ArrayBuffer
Using data.buffer can compress unintended bytes when data has non-zero byteOffset/length. Pass the Uint8Array directly.
Apply:
- return Bun.gzipSync(data.buffer as ArrayBuffer);
+ return Bun.gzipSync(data);
📝 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.
function compressDataIfAppropriate( | |
data: Uint8Array, | |
mimeType: string, | |
): Uint8Array | undefined { | |
if (!ENABLE_GZIP) return undefined; | |
if (data.byteLength < GZIP_MIN_BYTES) return undefined; | |
if (!isMimeTypeCompressible(mimeType)) return undefined; | |
try { | |
return Bun.gzipSync(data.buffer as ArrayBuffer); | |
} catch { | |
return undefined; | |
} | |
} | |
function compressDataIfAppropriate( | |
data: Uint8Array, | |
mimeType: string, | |
): Uint8Array | undefined { | |
if (!ENABLE_GZIP) return undefined; | |
if (data.byteLength < GZIP_MIN_BYTES) return undefined; | |
if (!isMimeTypeCompressible(mimeType)) return undefined; | |
try { | |
return Bun.gzipSync(data); | |
} catch { | |
return undefined; | |
} | |
} |
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts around lines 211 to 223, the gzip call
uses data.buffer which may include bytes outside the Uint8Array's
byteOffset/byteLength; replace that with a gzip invocation that only compresses
the Uint8Array's active bytes — either pass the Uint8Array directly to
Bun.gzipSync (if supported) or create an ArrayBuffer view/slice that represents
only data.byteOffset..data.byteOffset+data.byteLength before calling
Bun.gzipSync; keep the existing checks and try/catch.
examples/react/start-bun/server.ts
Outdated
return (req: Request) => { | ||
const headers: Record<string, string> = { | ||
"Content-Type": asset.type, | ||
"Cache-Control": asset.immutable | ||
? "public, max-age=31536000, immutable" | ||
: "public, max-age=3600", | ||
}; | ||
|
||
if (ENABLE_ETAG && asset.etag) { | ||
const ifNone = req.headers.get("if-none-match"); | ||
if (ifNone && ifNone === asset.etag) { | ||
return new Response(null, { | ||
status: 304, | ||
headers: { ETag: asset.etag }, | ||
}); | ||
} | ||
headers.ETag = asset.etag; | ||
} | ||
|
||
if ( | ||
ENABLE_GZIP && | ||
asset.gz && | ||
req.headers.get("accept-encoding")?.includes("gzip") | ||
) { | ||
headers["Content-Encoding"] = "gzip"; | ||
headers["Content-Length"] = String(asset.gz.byteLength); | ||
const gzCopy = new Uint8Array(asset.gz); | ||
return new Response(gzCopy, { status: 200, headers }); | ||
} | ||
|
||
headers["Content-Length"] = String(asset.raw.byteLength); | ||
const rawCopy = new Uint8Array(asset.raw); | ||
return new Response(rawCopy, { status: 200, headers }); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add Vary: Accept-Encoding for cache correctness
When conditionally serving gzip, responses must include Vary: Accept-Encoding to avoid cache poisoning.
Apply:
return (req: Request) => {
const headers: Record<string, string> = {
"Content-Type": asset.type,
"Cache-Control": asset.immutable
? "public, max-age=31536000, immutable"
: "public, max-age=3600",
+ "Vary": "Accept-Encoding",
};
@@
- return new Response(null, {
- status: 304,
- headers: { ETag: asset.etag },
- });
+ return new Response(null, {
+ status: 304,
+ headers: { ETag: asset.etag, Vary: "Accept-Encoding" },
+ });
📝 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.
return (req: Request) => { | |
const headers: Record<string, string> = { | |
"Content-Type": asset.type, | |
"Cache-Control": asset.immutable | |
? "public, max-age=31536000, immutable" | |
: "public, max-age=3600", | |
}; | |
if (ENABLE_ETAG && asset.etag) { | |
const ifNone = req.headers.get("if-none-match"); | |
if (ifNone && ifNone === asset.etag) { | |
return new Response(null, { | |
status: 304, | |
headers: { ETag: asset.etag }, | |
}); | |
} | |
headers.ETag = asset.etag; | |
} | |
if ( | |
ENABLE_GZIP && | |
asset.gz && | |
req.headers.get("accept-encoding")?.includes("gzip") | |
) { | |
headers["Content-Encoding"] = "gzip"; | |
headers["Content-Length"] = String(asset.gz.byteLength); | |
const gzCopy = new Uint8Array(asset.gz); | |
return new Response(gzCopy, { status: 200, headers }); | |
} | |
headers["Content-Length"] = String(asset.raw.byteLength); | |
const rawCopy = new Uint8Array(asset.raw); | |
return new Response(rawCopy, { status: 200, headers }); | |
}; | |
} | |
return (req: Request) => { | |
const headers: Record<string, string> = { | |
"Content-Type": asset.type, | |
"Cache-Control": asset.immutable | |
? "public, max-age=31536000, immutable" | |
: "public, max-age=3600", | |
"Vary": "Accept-Encoding", | |
}; | |
if (ENABLE_ETAG && asset.etag) { | |
const ifNone = req.headers.get("if-none-match"); | |
if (ifNone && ifNone === asset.etag) { | |
return new Response(null, { | |
status: 304, | |
headers: { ETag: asset.etag, Vary: "Accept-Encoding" }, | |
}); | |
} | |
headers.ETag = asset.etag; | |
} | |
if ( | |
ENABLE_GZIP && | |
asset.gz && | |
req.headers.get("accept-encoding")?.includes("gzip") | |
) { | |
headers["Content-Encoding"] = "gzip"; | |
headers["Content-Length"] = String(asset.gz.byteLength); | |
const gzCopy = new Uint8Array(asset.gz); | |
return new Response(gzCopy, { status: 200, headers }); | |
} | |
headers["Content-Length"] = String(asset.raw.byteLength); | |
const rawCopy = new Uint8Array(asset.raw); | |
return new Response(rawCopy, { status: 200, headers }); | |
}; | |
} |
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts around lines 231 to 265, the responses
that vary based on Accept-Encoding lack a Vary header which can poison caches;
ensure you set headers["Vary"] = "Accept-Encoding" whenever ENABLE_GZIP is
enabled and the request might be served gzip or not (add it before the ETag/304
check and include it on the 304 response as well), so both the gzipped response,
the plain response, and the 304 response include Vary: Accept-Encoding.
examples/react/start-bun/server.ts
Outdated
if (matchesPattern && withinSizeLimit) { | ||
// Preload small files into memory with ETag and Gzip support | ||
const bytes = new Uint8Array(await file.arrayBuffer()); | ||
const gz = compressDataIfAppropriate(bytes, metadata.type); | ||
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined; | ||
const asset: InMemoryAsset = { | ||
raw: bytes, | ||
gz, | ||
etag, | ||
type: metadata.type, | ||
immutable: true, | ||
size: bytes.byteLength, | ||
}; | ||
routes[route] = createResponseHandler(asset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Avoid immutable caching for HTML
Preloading HTML with immutable caching risks stale documents. Treat HTML as non-immutable.
Apply:
- const asset: InMemoryAsset = {
+ const isHtml = metadata.type.startsWith("text/html");
+ const asset: InMemoryAsset = {
raw: bytes,
gz,
etag,
type: metadata.type,
- immutable: true,
+ immutable: !isHtml,
size: bytes.byteLength,
};
📝 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.
if (matchesPattern && withinSizeLimit) { | |
// Preload small files into memory with ETag and Gzip support | |
const bytes = new Uint8Array(await file.arrayBuffer()); | |
const gz = compressDataIfAppropriate(bytes, metadata.type); | |
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined; | |
const asset: InMemoryAsset = { | |
raw: bytes, | |
gz, | |
etag, | |
type: metadata.type, | |
immutable: true, | |
size: bytes.byteLength, | |
}; | |
routes[route] = createResponseHandler(asset); | |
if (matchesPattern && withinSizeLimit) { | |
// Preload small files into memory with ETag and Gzip support | |
const bytes = new Uint8Array(await file.arrayBuffer()); | |
const gz = compressDataIfAppropriate(bytes, metadata.type); | |
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined; | |
const isHtml = metadata.type.startsWith("text/html"); | |
const asset: InMemoryAsset = { | |
raw: bytes, | |
gz, | |
etag, | |
type: metadata.type, | |
immutable: !isHtml, | |
size: bytes.byteLength, | |
}; | |
routes[route] = createResponseHandler(asset); |
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts around lines 336 to 349, the code marks
all preloaded assets as immutable, which will incorrectly cache HTML; change the
logic so that if metadata.type is "text/html" (or startsWith "text/html"), set
asset.immutable = false (leave other types immutable), and ensure any ETag/Gzip
handling remains unchanged; update the created asset object to compute immutable
based on the content type rather than hardcoding true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (5)
examples/react/start-bun/server.ts (3)
347-361
: Add pretty routes for prerendered HTML (/dir and /dir/ for /dir/index.html, and /foo for /foo.html)Improves UX and matches expectations from prerender suggestions.
- routes[route] = createResponseHandler(asset) + routes[route] = createResponseHandler(asset) + // Pretty routes for prerendered HTML + if (route.endsWith('/index.html')) { + const base = route.slice(0, -'/index.html'.length) || '/' + routes[base] = createResponseHandler(asset) + if (!base.endsWith('/')) { + routes[base + '/'] = createResponseHandler(asset) + } + } else if (route.endsWith('.html')) { + const pretty = route.slice(0, -'.html'.length) + routes[pretty] = createResponseHandler(asset) + } @@ - routes[route] = () => { - const fileOnDemand = Bun.file(filepath) - return new Response(fileOnDemand, { - headers: { - 'Content-Type': metadata.type, - 'Cache-Control': 'public, max-age=3600', - }, - }) - } + const mk = () => + new Response(Bun.file(filepath), { + headers: { + 'Content-Type': metadata.type, + 'Cache-Control': 'public, max-age=3600', + }, + }) + routes[route] = mk + // Pretty routes for prerendered HTML + if (route.endsWith('/index.html')) { + const base = route.slice(0, -'/index.html'.length) || '/' + routes[base] = mk + if (!base.endsWith('/')) { + routes[base + '/'] = mk + } + } else if (route.endsWith('.html')) { + const pretty = route.slice(0, -'.html'.length) + routes[pretty] = mk + }
126-136
: Avoid building regexes from user input; prefer Bun.Glob for filteringDynamic RegExp from env can risk ReDoS and subtle matching differences. Since Bun.Glob is already used for scanning, reuse it for include/exclude checks.
Proposed approach:
- Replace convertGlobToRegExp with Bun.Glob-based matchers and test against relativePath (not just filename).
- Example:
// near config const INCLUDE_GLOBS = (process.env.STATIC_PRELOAD_INCLUDE ?? '') .split(',').map(s => s.trim()).filter(Boolean) .map(p => new Bun.Glob(p)) const EXCLUDE_GLOBS = (process.env.STATIC_PRELOAD_EXCLUDE ?? '') .split(',').map(s => s.trim()).filter(Boolean) .map(p => new Bun.Glob(p)) function isFileEligibleForPreloading(relativePath: string): boolean { const included = INCLUDE_GLOBS.length === 0 || INCLUDE_GLOBS.some(g => g.match(relativePath)) const excluded = EXCLUDE_GLOBS.some(g => g.match(relativePath)) return included && !excluded }As per static analysis hints.
Also applies to: 179-195
248-257
: Optional: Parse Accept-Encoding q-valuesPlain includes('gzip') ignores q=0 and quality. Low risk, but easy to harden.
Snippet:
function acceptsGzip(h: string | null): boolean { if (!h) return false return h .split(',') .map(v => v.trim()) .map(v => { const [enc, q] = v.split(';').map(s => s.trim()) const qv = q?.startsWith('q=') ? Number(q.slice(2)) : 1 return { enc, q: Number.isFinite(qv) ? qv : 0 } }) .some(({ enc, q }) => enc === 'gzip' && q > 0) }Then use acceptsGzip(req.headers.get('accept-encoding')).
examples/react/start-bun/README.md (2)
86-101
: Docs align with STATIC_PRELOAD_ (good). Add advanced toggles to match server features*Consider documenting ETag and Gzip knobs exposed by server.ts for completeness.
STATIC_PRELOAD_VERBOSE=true + +# Enable/disable ETag (default: true) +STATIC_PRELOAD_ETAG=true + +# Enable/disable gzip (default: true) +STATIC_PRELOAD_GZIP=true + +# Gzip thresholds and types +STATIC_PRELOAD_GZIP_MIN_BYTES=1024 +STATIC_PRELOAD_GZIP_TYPES="text/,application/javascript,application/json,application/xml,image/svg+xml"
43-55
: Clarify 404 when no HTML is emittedIf dist/client has no HTML, routes are served by the SSR handler (dist/server/server.js). A 404 at “/” typically indicates the app’s route didn’t match, not a static-server issue. Add a sentence to reduce confusion.
Proposed text:
- If your build does not emit HTML files (common with SSR), the server falls back to the TanStack Start handler for routes like “/”. A 404 likely means the application router did not match; verify your routes and server entry were built and imported correctly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/server.ts
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/server.ts
examples/react/start-bun/README.md
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 134-134: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (2)
examples/react/start-bun/server.ts (2)
209-221
: Fix gzip bug: compress only the active bytesUsing data.buffer can include unrelated bytes if the view has an offset. Pass the Uint8Array directly.
- return Bun.gzipSync(data.buffer as ArrayBuffer) + return Bun.gzipSync(data)
339-346
: Don’t mark HTML as immutableImmutable caching for HTML risks stale pages. Treat HTML as non-immutable.
- const asset: InMemoryAsset = { + const isHtml = metadata.type.startsWith('text/html') + const asset: InMemoryAsset = { raw: bytes, gz, etag, type: metadata.type, - immutable: true, + immutable: !isHtml, size: bytes.byteLength, }
* ASSET_PRELOAD_MAX_SIZE (number) | ||
* - Maximum file size in bytes to preload into memory | ||
* - Files larger than this will be served on-demand from disk | ||
* - Default: 5242880 (5MB) | ||
* - Example: ASSET_PRELOAD_MAX_SIZE=5242880 (5MB) | ||
* | ||
* ASSET_PRELOAD_INCLUDE_PATTERNS (string) | ||
* - Comma-separated list of glob patterns for files to include | ||
* - If specified, only matching files are eligible for preloading | ||
* - Patterns are matched against filenames only, not full paths | ||
* - Example: ASSET_PRELOAD_INCLUDE_PATTERNS="*.js,*.css,*.woff2" | ||
* | ||
* ASSET_PRELOAD_EXCLUDE_PATTERNS (string) | ||
* - Comma-separated list of glob patterns for files to exclude | ||
* - Applied after include patterns | ||
* - Patterns are matched against filenames only, not full paths | ||
* - Example: ASSET_PRELOAD_EXCLUDE_PATTERNS="*.map,*.txt" | ||
* | ||
* ASSET_PRELOAD_VERBOSE_LOGGING (boolean) | ||
* - Enable detailed logging of loaded and skipped files | ||
* - Default: false | ||
* - Set to "true" to enable verbose output | ||
* | ||
* ASSET_PRELOAD_ENABLE_ETAG (boolean) | ||
* - Enable ETag generation for preloaded assets | ||
* - Default: true | ||
* - Set to "false" to disable ETag support | ||
* | ||
* ASSET_PRELOAD_ENABLE_GZIP (boolean) | ||
* - Enable Gzip compression for eligible assets | ||
* - Default: true | ||
* - Set to "false" to disable Gzip compression | ||
* | ||
* ASSET_PRELOAD_GZIP_MIN_SIZE (number) | ||
* - Minimum file size in bytes required for Gzip compression | ||
* - Files smaller than this will not be compressed | ||
* - Default: 1024 (1KB) | ||
* | ||
* ASSET_PRELOAD_GZIP_MIME_TYPES (string) | ||
* - Comma-separated list of MIME types eligible for Gzip compression | ||
* - Supports partial matching for types ending with "/" | ||
* - Default: text/,application/javascript,application/json,application/xml,image/svg+xml | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Env var names drift from docs (ASSET_PRELOAD_ vs STATIC_PRELOAD_*)*
Code uses ASSET_PRELOAD_* while README/docs use STATIC_PRELOAD_*. This will cause configs to be ignored and confuse users.
Apply this doc-block update to align names with docs:
- * ASSET_PRELOAD_MAX_SIZE (number)
+ * STATIC_PRELOAD_MAX_BYTES (number)
@@
- * ASSET_PRELOAD_INCLUDE_PATTERNS (string)
+ * STATIC_PRELOAD_INCLUDE (string)
@@
- * ASSET_PRELOAD_EXCLUDE_PATTERNS (string)
+ * STATIC_PRELOAD_EXCLUDE (string)
@@
- * ASSET_PRELOAD_VERBOSE_LOGGING (boolean)
+ * STATIC_PRELOAD_VERBOSE (boolean)
@@
- * ASSET_PRELOAD_ENABLE_ETAG (boolean)
+ * STATIC_PRELOAD_ETAG (boolean)
@@
- * ASSET_PRELOAD_ENABLE_GZIP (boolean)
+ * STATIC_PRELOAD_GZIP (boolean)
@@
- * ASSET_PRELOAD_GZIP_MIN_SIZE (number)
+ * STATIC_PRELOAD_GZIP_MIN_BYTES (number)
@@
- * ASSET_PRELOAD_GZIP_MIME_TYPES (string)
+ * STATIC_PRELOAD_GZIP_TYPES (string)
🏁 Script executed:
#!/bin/bash
# Verify no lingering ASSET_PRELOAD_ after changes
rg -n --type=ts 'ASSET_PRELOAD_' -C1
Length of output: 4813
🏁 Script executed:
#!/bin/bash
# Search for STATIC_PRELOAD_ references in documentation and code
rg -n 'STATIC_PRELOAD_' -C1
Length of output: 2942
Rename env var names in code and docs to STATIC_PRELOAD_*
Env vars in server.ts (doc-block and process.env
calls) and examples/react/start-bun/README.md still use ASSET_PRELOAD_*
but all docs (docs/start/...
) expect STATIC_PRELOAD_*
, so configurations will be ignored. Rename every ASSET_PRELOAD_MAX_SIZE → STATIC_PRELOAD_MAX_BYTES
, ASSET_PRELOAD_INCLUDE_PATTERNS → STATIC_PRELOAD_INCLUDE
, etc., in:
- the comment block in examples/react/start-bun/server.ts
- all
process.env.ASSET_PRELOAD_*
usages in server.ts - environment examples in examples/react/start-bun/README.md
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts lines 19-61, the doc-block and environment
variable usages must be renamed from ASSET_PRELOAD_* to STATIC_PRELOAD_* so
runtime config matches docs: update the comment block names and every
process.env.ASSET_PRELOAD_* reference to the new names (use these mappings:
ASSET_PRELOAD_MAX_SIZE → STATIC_PRELOAD_MAX_BYTES,
ASSET_PRELOAD_INCLUDE_PATTERNS → STATIC_PRELOAD_INCLUDE,
ASSET_PRELOAD_EXCLUDE_PATTERNS → STATIC_PRELOAD_EXCLUDE,
ASSET_PRELOAD_VERBOSE_LOGGING → STATIC_PRELOAD_VERBOSE,
ASSET_PRELOAD_ENABLE_ETAG → STATIC_PRELOAD_ENABLE_ETAG,
ASSET_PRELOAD_ENABLE_GZIP → STATIC_PRELOAD_ENABLE_GZIP,
ASSET_PRELOAD_GZIP_MIN_SIZE → STATIC_PRELOAD_GZIP_MIN_BYTES,
ASSET_PRELOAD_GZIP_MIME_TYPES → STATIC_PRELOAD_GZIP_MIME_TYPES), and make the
same name changes in examples/react/start-bun/README.md environment examples so
the docs and code are consistent.
const MAX_PRELOAD_BYTES = Number( | ||
process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default | ||
) | ||
|
||
// Parse comma-separated include patterns (no defaults) | ||
const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '') | ||
.split(',') | ||
.map((s) => s.trim()) | ||
.filter(Boolean) | ||
.map((pattern: string) => convertGlobToRegExp(pattern)) | ||
|
||
// Parse comma-separated exclude patterns (no defaults) | ||
const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '') | ||
.split(',') | ||
.map((s) => s.trim()) | ||
.filter(Boolean) | ||
.map((pattern: string) => convertGlobToRegExp(pattern)) | ||
|
||
// Verbose logging flag | ||
const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true' | ||
|
||
// Optional ETag feature | ||
const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true' | ||
|
||
// Optional Gzip feature | ||
const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true' | ||
const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) // 1KB | ||
const GZIP_TYPES = ( | ||
process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ?? | ||
'text/,application/javascript,application/json,application/xml,image/svg+xml' | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use STATIC_PRELOAD_ env vars in code to match docs*
Replace all ASSET_PRELOAD_* usages. This ensures user-provided env works as documented.
Apply:
-const MAX_PRELOAD_BYTES = Number(
- process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default
-)
+const MAX_PRELOAD_BYTES = Number(
+ process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, // 5MB default
+)
-const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
+const INCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_INCLUDE ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
-const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '')
+const EXCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_EXCLUDE ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
-const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true'
+const VERBOSE = process.env.STATIC_PRELOAD_VERBOSE === 'true'
-const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true'
+const ENABLE_ETAG = (process.env.STATIC_PRELOAD_ETAG ?? 'true') === 'true'
-const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true'
-const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) // 1KB
+const ENABLE_GZIP = (process.env.STATIC_PRELOAD_GZIP ?? 'true') === 'true'
+const GZIP_MIN_BYTES = Number(process.env.STATIC_PRELOAD_GZIP_MIN_BYTES ?? 1024) // 1KB
const GZIP_TYPES = (
- process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ??
+ process.env.STATIC_PRELOAD_GZIP_TYPES ??
'text/,application/javascript,application/json,application/xml,image/svg+xml'
)
@@
-function createCompositeGlobPattern(): Bun.Glob {
- const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
+function createCompositeGlobPattern(): Bun.Glob {
+ const raw = (process.env.STATIC_PRELOAD_INCLUDE ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (raw.length === 0) return new Bun.Glob('**/*')
if (raw.length === 1) return new Bun.Glob(raw[0])
return new Bun.Glob(`{${raw.join(',')}}`)
}
@@
- `Include patterns: ${process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? ''}`,
+ `Include patterns: ${process.env.STATIC_PRELOAD_INCLUDE ?? ''}`,
@@
- `Exclude patterns: ${process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? ''}`,
+ `Exclude patterns: ${process.env.STATIC_PRELOAD_EXCLUDE ?? ''}`,
Also applies to: 268-276, 295-304
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts around lines 91 to 121 (also apply same
change at 268-276 and 295-304): the code reads environment variables prefixed
with ASSET_PRELOAD_* but the docs and expected API use STATIC_PRELOAD_*; update
each env var reference to use the STATIC_PRELOAD_* names (e.g.
STATIC_PRELOAD_MAX_SIZE, STATIC_PRELOAD_INCLUDE_PATTERNS,
STATIC_PRELOAD_EXCLUDE_PATTERNS, STATIC_PRELOAD_VERBOSE_LOGGING,
STATIC_PRELOAD_ENABLE_ETAG, STATIC_PRELOAD_ENABLE_GZIP,
STATIC_PRELOAD_GZIP_MIN_SIZE, STATIC_PRELOAD_GZIP_MIME_TYPES) keeping current
defaults and parsing behavior intact so functionality remains unchanged.
return (req: Request) => { | ||
const headers: Record<string, string> = { | ||
'Content-Type': asset.type, | ||
'Cache-Control': asset.immutable | ||
? 'public, max-age=31536000, immutable' | ||
: 'public, max-age=3600', | ||
} | ||
|
||
if (ENABLE_ETAG && asset.etag) { | ||
const ifNone = req.headers.get('if-none-match') | ||
if (ifNone && ifNone === asset.etag) { | ||
return new Response(null, { | ||
status: 304, | ||
headers: { ETag: asset.etag }, | ||
}) | ||
} | ||
headers.ETag = asset.etag | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add Vary: Accept-Encoding for gzip negotiation (and 304)
Responses that conditionally serve gzip must include Vary: Accept-Encoding for cache correctness.
return (req: Request) => {
const headers: Record<string, string> = {
'Content-Type': asset.type,
'Cache-Control': asset.immutable
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600',
}
+ if (ENABLE_GZIP) {
+ headers['Vary'] = 'Accept-Encoding'
+ }
if (ENABLE_ETAG && asset.etag) {
const ifNone = req.headers.get('if-none-match')
if (ifNone && ifNone === asset.etag) {
- return new Response(null, {
- status: 304,
- headers: { ETag: asset.etag },
- })
+ return new Response(null, {
+ status: 304,
+ headers: ENABLE_GZIP
+ ? { ETag: asset.etag, 'Vary': 'Accept-Encoding' }
+ : { ETag: asset.etag },
+ })
}
headers.ETag = asset.etag
}
Also applies to: 239-243
🤖 Prompt for AI Agents
In examples/react/start-bun/server.ts around lines 229 to 246 (and also apply
same change to lines 239-243), the response headers omit "Vary: Accept-Encoding"
which is required when serving conditional gzip content; update the code so that
whenever you set headers for the asset (including when adding ETag) you also add
or append "Vary: Accept-Encoding" to the headers object, and ensure the 304
Response returned when if-none-match matches also includes the "Vary:
Accept-Encoding" header (not just ETag) so caches vary correctly by
Accept-Encoding.
Adds a complete TanStack Start + Bun example with production-ready server implementation that demonstrates how to deploy TanStack Start apps with Bun. Updates documentation to reference the in-repo example instead of external repository. The example includes: - Optimized production server with intelligent static asset loading - Configurable memory management with hybrid loading strategy - Complete project setup with TypeScript, ESLint, and Prettier configs - README with detailed usage instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
…nd logging This update refines the Bun server implementation in the TanStack Start example by: - Renaming environment variables for clarity and consistency. - Introducing new features for asset preloading, including ETag generation and Gzip compression. - Enhancing logging capabilities for better visibility during server operations. - Refactoring functions for improved readability and maintainability. These changes aim to provide a more robust and user-friendly server experience for deploying TanStack Start applications with Bun.
c14260b
to
e06b806
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
examples/react/start-bun/bun.lock
is excluded by!**/*.lock
examples/react/start-bun/public/favicon.ico
is excluded by!**/*.ico
examples/react/start-bun/public/logo192.png
is excluded by!**/*.png
examples/react/start-bun/public/logo512.png
is excluded by!**/*.png
examples/react/start-bun/src/logo.svg
is excluded by!**/*.svg
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
docs/start/framework/react/hosting.md
(2 hunks)examples/react/start-bun/.gitignore
(1 hunks)examples/react/start-bun/.prettierignore
(1 hunks)examples/react/start-bun/.vscode/settings.json
(1 hunks)examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/eslint.config.js
(1 hunks)examples/react/start-bun/package.json
(1 hunks)examples/react/start-bun/prettier.config.js
(1 hunks)examples/react/start-bun/public/manifest.json
(1 hunks)examples/react/start-bun/public/robots.txt
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)examples/react/start-bun/src/components/Header.tsx
(1 hunks)examples/react/start-bun/src/routeTree.gen.ts
(1 hunks)examples/react/start-bun/src/router.tsx
(1 hunks)examples/react/start-bun/src/routes/__root.tsx
(1 hunks)examples/react/start-bun/src/routes/api.demo-names.ts
(1 hunks)examples/react/start-bun/src/routes/demo.start.api-request.tsx
(1 hunks)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
(1 hunks)examples/react/start-bun/src/routes/index.tsx
(1 hunks)examples/react/start-bun/src/styles.css
(1 hunks)examples/react/start-bun/tsconfig.json
(1 hunks)examples/react/start-bun/vite.config.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (16)
- examples/react/start-bun/package.json
- examples/react/start-bun/.gitignore
- examples/react/start-bun/src/components/Header.tsx
- examples/react/start-bun/eslint.config.js
- examples/react/start-bun/src/routes/demo.start.api-request.tsx
- examples/react/start-bun/public/manifest.json
- examples/react/start-bun/prettier.config.js
- examples/react/start-bun/src/routes/index.tsx
- examples/react/start-bun/public/robots.txt
- examples/react/start-bun/src/router.tsx
- examples/react/start-bun/src/routeTree.gen.ts
- examples/react/start-bun/tsconfig.json
- examples/react/start-bun/src/routes/demo.start.server-funcs.tsx
- examples/react/start-bun/src/styles.css
- examples/react/start-bun/.vscode/settings.json
- examples/react/start-bun/.prettierignore
🧰 Additional context used
📓 Path-based instructions (5)
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Files:
docs/start/framework/react/hosting.md
docs/{router,start}/**
📄 CodeRabbit inference engine (AGENTS.md)
Place router docs under docs/router/ and start framework docs under docs/start/
Files:
docs/start/framework/react/hosting.md
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/server.ts
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/vite.config.ts
**/src/routes/**
📄 CodeRabbit inference engine (AGENTS.md)
Place file-based routes under src/routes/ directories
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/src/routes/api.demo-names.ts
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/src/routes/__root.tsx
examples/react/start-bun/README.md
examples/react/start-bun/server.ts
examples/react/start-bun/src/routes/api.demo-names.ts
examples/react/start-bun/vite.config.ts
🧠 Learnings (1)
📚 Learning: 2025-09-23T17:36:12.598Z
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to docs/**/*.{md,mdx} : Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Applied to files:
docs/start/framework/react/hosting.md
🧬 Code graph analysis (2)
examples/react/start-bun/src/routes/__root.tsx (2)
examples/react/start-bun/src/components/Header.tsx (1)
Header
(3-21)packages/react-router-devtools/src/TanStackRouterDevtoolsPanel.tsx (1)
TanStackRouterDevtoolsPanel
(37-87)
examples/react/start-bun/src/routes/api.demo-names.ts (4)
examples/react/start-bun/src/routes/__root.tsx (1)
Route
(9-32)examples/react/start-bun/src/routes/demo.start.api-request.tsx (1)
Route
(9-11)examples/react/start-bun/src/routes/demo.start.server-funcs.tsx (1)
Route
(36-39)examples/react/start-bun/src/routes/index.tsx (1)
Route
(4-6)
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 134-134: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (4)
docs/start/framework/react/hosting.md (1)
248-310
: Replace the deep relative link with a stable GitHub URLLinking out of
docs/
via../../../../
breaks the docs build and violates the docs linking guideline. Please swap it for a stable GitHub URL.-1. Copy the [`server.ts`](../../../../examples/react/start-bun/server.ts) file from the example in this repository to your project root +1. Copy the `server.ts` file from the example in this repository to your project root: + https://github.com/TanStack/router/blob/main/examples/react/start-bun/server.tsAs per coding guidelines
examples/react/start-bun/server.ts (3)
19-121
: Restore STATIC_PRELOAD_ env var contract*Docs in this PR instruct users to set
STATIC_PRELOAD_*
, but the code and docblock here still expectASSET_PRELOAD_*
. As shipped, none of the documented env vars will be honored, so preloading/gzip/etag tuning silently fails. Please rename every remainingASSET_PRELOAD_*
reference (including the comment and env reads) to the matchingSTATIC_PRELOAD_*
symbols to keep the runtime in sync with the docs.-const MAX_PRELOAD_BYTES = Number( - process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, -) +const MAX_PRELOAD_BYTES = Number( + process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, +)…and apply the same rename for
INCLUDE/EXCLUDE
,VERBOSE
,ETAG
,GZIP
,GZIP_MIN
, andGZIP_TYPES
. Update the comment block accordingly.
229-257
: EmitVary: Accept-Encoding
when negotiating gzipThese handlers serve different representations (plain vs gzip) based solely on
Accept-Encoding
, but the responses lackVary: Accept-Encoding
. Shared caches can therefore hand a gzipped payload to a client that didn’t request gzip (or vice versa). Add theVary
header for both the 200 and 304 paths when gzip is enabled.return (req: Request) => { const headers: Record<string, string> = { 'Content-Type': asset.type, 'Cache-Control': asset.immutable ? 'public, max-age=31536000, immutable' : 'public, max-age=3600', } + if (ENABLE_GZIP) { + headers['Vary'] = 'Accept-Encoding' + } if (ENABLE_ETAG && asset.etag) { const ifNone = req.headers.get('if-none-match') if (ifNone && ifNone === asset.etag) { return new Response(null, { status: 304, - headers: { ETag: asset.etag }, + headers: ENABLE_GZIP + ? { ETag: asset.etag, 'Vary': 'Accept-Encoding' } + : { ETag: asset.etag }, }) }
340-345
: Don’t mark HTML responses as immutablePreloaded HTML is currently tagged with
immutable
caching (one-year TTL). Any content deploy will stay stale in downstream caches/CDNs until they’re manually purged. Detecttext/html
when buildingasset
and forceimmutable: false
(keeping immutable for CSS/JS/etc.).- const asset: InMemoryAsset = { + const isHtml = metadata.type.startsWith('text/html') + const asset: InMemoryAsset = { raw: bytes, gz, etag, type: metadata.type, - immutable: true, + immutable: !isHtml, size: bytes.byteLength, }
import { TanStackDevtools } from '@tanstack/react-devtools' | ||
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' | ||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' | ||
|
||
import Header from '../components/Header' | ||
|
||
import appCss from '../styles.css?url' | ||
|
||
export const Route = createRootRoute({ | ||
head: () => ({ | ||
meta: [ | ||
{ | ||
charSet: 'utf-8', | ||
}, | ||
{ | ||
name: 'viewport', | ||
content: 'width=device-width, initial-scale=1', | ||
}, | ||
{ | ||
title: 'TanStack Start Starter', | ||
}, | ||
], | ||
links: [ | ||
{ | ||
rel: 'stylesheet', | ||
href: appCss, | ||
}, | ||
], | ||
}), | ||
|
||
shellComponent: RootDocument, | ||
}) | ||
|
||
function RootDocument({ children }: { children: React.ReactNode }) { | ||
return ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import React types for the shell component signature
RootDocument
references React.ReactNode
, but this file never imports React
. TypeScript will emit an error here under strict mode. Pull in the type (or switch to ReactNode
) so the file compiles.
+import type { ReactNode } from 'react'
function RootDocument({ children }: { children: React.ReactNode }) {
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In examples/react/start-bun/src/routes/__root.tsx around lines 1 to 35, the
RootDocument component uses React.ReactNode but React is not imported, causing a
TypeScript error; add a type-only import such as "import type { ReactNode } from
'react'" at the top and update the component signature to use ReactNode (e.g. ({
children }: { children: ReactNode })) or alternatively import React itself if
preferred.
e06b806
to
4e16af0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
examples/react/start-bun/server.ts (3)
248-256
: Prefer robust Accept-Encoding parsing (respect q=0).includes('gzip') can gzip even when q=0. Parse properly.
- if ( - ENABLE_GZIP && - asset.gz && - req.headers.get('accept-encoding')?.includes('gzip') - ) { + if (ENABLE_GZIP && asset.gz && clientAcceptsGzip(req.headers)) {Add helper (outside function):
function clientAcceptsGzip(headers: Headers): boolean { const ae = headers.get('accept-encoding')?.toLowerCase() ?? '' if (!ae) return false for (const token of ae.split(',')) { const [enc, ...params] = token.trim().split(';') if (enc === 'gzip') { const q = params.find((p) => p.trim().startsWith('q=')) if (!q) return true const v = Number(q.split('=')[1]) return isFinite(v) && v > 0 } } return false }
67-67
: Guard against NaN PORT values.Ensure invalid PORT doesn’t yield NaN.
-const SERVER_PORT = Number(process.env.PORT ?? 3000) +const SERVER_PORT = + Number.parseInt(process.env.PORT ?? '3000', 10) || 3000
347-354
: Add clean routes for prerendered HTML (index.html and .html without suffix).Optional UX: map /foo/index.html → /foo and /foo/, and /page.html → /page. Helps static exports.
- routes[route] = createResponseHandler(asset) + const handler = createResponseHandler(asset) + routes[route] = handler + addHtmlAliases(route, handler, routes) @@ - routes[route] = () => { + const onDemand = () => { const fileOnDemand = Bun.file(filepath) return new Response(fileOnDemand, { headers: { 'Content-Type': metadata.type, 'Cache-Control': 'public, max-age=3600', }, }) } + routes[route] = onDemand + addHtmlAliases(route, onDemand, routes)Add helper (outside):
function addHtmlAliases( route: string, handler: (req: Request) => Response | Promise<Response>, into: Record<string, (req: Request) => Response | Promise<Response>>, ) { if (route.endsWith('/index.html')) { const base = route.slice(0, -'/index.html'.length) || '/' into[base] = handler if (base !== '/' && base.endsWith('/')) into[base.slice(0, -1)] = handler } else if (route.endsWith('.html')) { const noExt = route.slice(0, -'.html'.length) into[noExt] = handler } }Also applies to: 353-361
docs/start/framework/react/hosting.md (1)
285-293
: Document the remaining STATIC_PRELOAD_ variables (ETag/Gzip).*Server supports ETAG and GZIP controls; add them for completeness.
**Environment Variables:** - `PORT`: Server port (default: 3000) - `STATIC_PRELOAD_MAX_BYTES`: Maximum file size to preload in bytes (default: 5242880 = 5MB) - `STATIC_PRELOAD_INCLUDE`: Comma-separated glob patterns for files to include - `STATIC_PRELOAD_EXCLUDE`: Comma-separated glob patterns for files to exclude - `STATIC_PRELOAD_VERBOSE`: Enable detailed logging (set to "true") +- `STATIC_PRELOAD_ETAG`: Enable ETag generation for preloaded assets (default: "true") +- `STATIC_PRELOAD_GZIP`: Enable gzip precompression and delivery (default: "true") +- `STATIC_PRELOAD_GZIP_MIN_BYTES`: Minimum bytes to gzip (default: 1024) +- `STATIC_PRELOAD_GZIP_TYPES`: Comma-separated MIME types eligible for gzip
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (3)
docs/start/framework/react/hosting.md
(2 hunks)examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- examples/react/start-bun/README.md
🧰 Additional context used
📓 Path-based instructions (4)
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Files:
docs/start/framework/react/hosting.md
docs/{router,start}/**
📄 CodeRabbit inference engine (AGENTS.md)
Place router docs under docs/router/ and start framework docs under docs/start/
Files:
docs/start/framework/react/hosting.md
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/server.ts
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/server.ts
🧠 Learnings (1)
📚 Learning: 2025-09-23T17:36:12.598Z
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to docs/**/*.{md,mdx} : Use internal docs links relative to the docs/ folder (e.g., ./guide/data-loading)
Applied to files:
docs/start/framework/react/hosting.md
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 134-134: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test
🔇 Additional comments (6)
docs/start/framework/react/hosting.md (2)
216-218
: Prefer bun add and a caret range for React 19.Use bun add and ^19 to allow non-breaking updates.
-```sh -bun install react@19 react-dom@19 +```sh +bun add react@^19 react-dom@^19
248-250
: Avoid deep relative links outside docs/; link to stable GitHub paths.Relative traversal often breaks on the docs site. Use absolute GitHub URLs.
-1. Copy the [`server.ts`](../../../../examples/react/start-bun/server.ts) file from the example in this repository to your project root +1. Copy the `server.ts` file from the example in this repository to your project root: + https://github.com/TanStack/router/blob/main/examples/react/start-bun/server.ts-For a complete working example, check out the [TanStack Start + Bun example](../../../../examples/react/start-bun) in this repository. +For a complete working example, see: +https://github.com/TanStack/router/tree/main/examples/react/start-bunAs per coding guidelines
Also applies to: 310-311
examples/react/start-bun/server.ts (4)
19-61
: Env var names drift from docs; rename ASSET_PRELOAD_ → STATIC_PRELOAD_ (breaks config).**Docs use STATIC_PRELOAD_* while code and header comment still use ASSET_PRELOAD_*. Users’ env won’t be picked up. Rename everywhere for consistency.
Apply these diffs:
@@ - * ASSET_PRELOAD_MAX_SIZE (number) + * STATIC_PRELOAD_MAX_BYTES (number) @@ - * - Example: ASSET_PRELOAD_MAX_SIZE=5242880 (5MB) + * - Example: STATIC_PRELOAD_MAX_BYTES=5242880 (5MB) @@ - * ASSET_PRELOAD_INCLUDE_PATTERNS (string) + * STATIC_PRELOAD_INCLUDE (string) @@ - * - Example: ASSET_PRELOAD_INCLUDE_PATTERNS="*.js,*.css,*.woff2" + * - Example: STATIC_PRELOAD_INCLUDE="*.js,*.css,*.woff2" @@ - * ASSET_PRELOAD_EXCLUDE_PATTERNS (string) + * STATIC_PRELOAD_EXCLUDE (string) @@ - * - Example: ASSET_PRELOAD_EXCLUDE_PATTERNS="*.map,*.txt" + * - Example: STATIC_PRELOAD_EXCLUDE="*.map,*.txt" @@ - * ASSET_PRELOAD_VERBOSE_LOGGING (boolean) + * STATIC_PRELOAD_VERBOSE (boolean) @@ - * ASSET_PRELOAD_ENABLE_ETAG (boolean) + * STATIC_PRELOAD_ETAG (boolean) @@ - * ASSET_PRELOAD_ENABLE_GZIP (boolean) + * STATIC_PRELOAD_GZIP (boolean) @@ - * ASSET_PRELOAD_GZIP_MIN_SIZE (number) + * STATIC_PRELOAD_GZIP_MIN_BYTES (number) @@ - * ASSET_PRELOAD_GZIP_MIME_TYPES (string) + * STATIC_PRELOAD_GZIP_TYPES (string)@@ -const MAX_PRELOAD_BYTES = Number( - process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default -) +const MAX_PRELOAD_BYTES = Number( + process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, // 5MB default +) @@ -const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '') +const INCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_INCLUDE ?? '') @@ -const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '') +const EXCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_EXCLUDE ?? '') @@ -const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true' +const VERBOSE = process.env.STATIC_PRELOAD_VERBOSE === 'true' @@ -const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true' +const ENABLE_ETAG = (process.env.STATIC_PRELOAD_ETAG ?? 'true') === 'true' @@ -const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true' -const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) // 1KB +const ENABLE_GZIP = (process.env.STATIC_PRELOAD_GZIP ?? 'true') === 'true' +const GZIP_MIN_BYTES = Number(process.env.STATIC_PRELOAD_GZIP_MIN_BYTES ?? 1024) // 1KB const GZIP_TYPES = ( - process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ?? + process.env.STATIC_PRELOAD_GZIP_TYPES ?? 'text/,application/javascript,application/json,application/xml,image/svg+xml' )@@ -function createCompositeGlobPattern(): Bun.Glob { - const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '') +function createCompositeGlobPattern(): Bun.Glob { + const raw = (process.env.STATIC_PRELOAD_INCLUDE ?? '')@@ - `Include patterns: ${process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? ''}`, + `Include patterns: ${process.env.STATIC_PRELOAD_INCLUDE ?? ''}`, @@ - `Exclude patterns: ${process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? ''}`, + `Exclude patterns: ${process.env.STATIC_PRELOAD_EXCLUDE ?? ''}`,Run to ensure no ASSET_PRELOAD_* remain:
#!/bin/bash rg -n --type=ts 'ASSET_PRELOAD_' -C1Also applies to: 91-121, 268-276, 295-304
209-221
: Gzip bug: compressing data.buffer can include extra bytes.If data has a non-zero byteOffset, data.buffer compresses unintended bytes. Pass Uint8Array directly.
- return Bun.gzipSync(data.buffer as ArrayBuffer) + return Bun.gzipSync(data)
229-246
: Missing Vary: Accept-Encoding for gzip negotiation (and 304).Conditional gzip must include Vary to prevent cache poisoning. Include it also on 304.
return (req: Request) => { const headers: Record<string, string> = { 'Content-Type': asset.type, 'Cache-Control': asset.immutable ? 'public, max-age=31536000, immutable' : 'public, max-age=3600', } + if (ENABLE_GZIP) { + headers['Vary'] = 'Accept-Encoding' + } if (ENABLE_ETAG && asset.etag) { const ifNone = req.headers.get('if-none-match') if (ifNone && ifNone === asset.etag) { - return new Response(null, { - status: 304, - headers: { ETag: asset.etag }, - }) + return new Response(null, { + status: 304, + headers: ENABLE_GZIP + ? { ETag: asset.etag, 'Vary': 'Accept-Encoding' } + : { ETag: asset.etag }, + }) } headers.ETag = asset.etag }Also applies to: 239-243
339-346
: Don’t mark HTML as immutable.HTML should not get long immutable caching. Use shorter caching for HTML.
- const asset: InMemoryAsset = { + const isHtml = metadata.type.startsWith('text/html') + const asset: InMemoryAsset = { raw: bytes, gz, etag, type: metadata.type, - immutable: true, + immutable: !isHtml, size: bytes.byteLength, }
8abb867
to
18884e1
Compare
More templates
@tanstack/arktype-adapter
@tanstack/directive-functions-plugin
@tanstack/eslint-plugin-router
@tanstack/history
@tanstack/nitro-v2-vite-plugin
@tanstack/react-router
@tanstack/react-router-devtools
@tanstack/react-router-ssr-query
@tanstack/react-start
@tanstack/react-start-client
@tanstack/react-start-server
@tanstack/router-cli
@tanstack/router-core
@tanstack/router-devtools
@tanstack/router-devtools-core
@tanstack/router-generator
@tanstack/router-plugin
@tanstack/router-ssr-query-core
@tanstack/router-utils
@tanstack/router-vite-plugin
@tanstack/server-functions-plugin
@tanstack/solid-router
@tanstack/solid-router-devtools
@tanstack/solid-start
@tanstack/solid-start-client
@tanstack/solid-start-server
@tanstack/start-client-core
@tanstack/start-plugin-core
@tanstack/start-server-core
@tanstack/start-static-server-functions
@tanstack/start-storage-context
@tanstack/valibot-adapter
@tanstack/virtual-file-routes
@tanstack/zod-adapter
commit: |
2f63984
to
3160df5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
examples/react/start-bun/README.md
(1 hunks)examples/react/start-bun/package.json
(1 hunks)examples/react/start-bun/server.ts
(1 hunks)examples/react/start-convex-trellaux/package.json
(1 hunks)examples/react/start-trellaux/package.json
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- examples/react/start-trellaux/package.json
- examples/react/start-convex-trellaux/package.json
🧰 Additional context used
📓 Path-based instructions (3)
**/package.json
📄 CodeRabbit inference engine (AGENTS.md)
Use workspace:* protocol for internal dependencies in package.json files
Files:
examples/react/start-bun/package.json
examples/{react,solid}/**
📄 CodeRabbit inference engine (AGENTS.md)
Keep example applications under examples/react/ and examples/solid/
Files:
examples/react/start-bun/package.json
examples/react/start-bun/README.md
examples/react/start-bun/server.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript in strict mode with extensive type safety across the codebase
Files:
examples/react/start-bun/server.ts
🪛 ast-grep (0.39.5)
examples/react/start-bun/server.ts
[warning] 134-134: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${escapedPattern}$
, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (3)
examples/react/start-bun/server.ts (3)
19-121
: Align env var names with documentedSTATIC_PRELOAD_*
keysThe README (and hosting docs) instruct users to set
STATIC_PRELOAD_*
, but the server still readsASSET_PRELOAD_*
, so every documented knob silently does nothing. Please bring the comment block and code back in sync with the documented names.- * ASSET_PRELOAD_MAX_SIZE (number) + * STATIC_PRELOAD_MAX_BYTES (number) @@ - * ASSET_PRELOAD_INCLUDE_PATTERNS (string) + * STATIC_PRELOAD_INCLUDE (string) @@ - * ASSET_PRELOAD_EXCLUDE_PATTERNS (string) + * STATIC_PRELOAD_EXCLUDE (string) @@ - * ASSET_PRELOAD_VERBOSE_LOGGING (boolean) + * STATIC_PRELOAD_VERBOSE (boolean) @@ - * ASSET_PRELOAD_ENABLE_ETAG (boolean) + * STATIC_PRELOAD_ETAG (boolean) @@ - * ASSET_PRELOAD_ENABLE_GZIP (boolean) + * STATIC_PRELOAD_GZIP (boolean) @@ - * ASSET_PRELOAD_GZIP_MIN_SIZE (number) + * STATIC_PRELOAD_GZIP_MIN_BYTES (number) @@ - * ASSET_PRELOAD_GZIP_MIME_TYPES (string) + * STATIC_PRELOAD_GZIP_TYPES (string) @@ - process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB default + process.env.STATIC_PRELOAD_MAX_BYTES ?? 5 * 1024 * 1024, // 5MB default @@ -const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '') +const INCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_INCLUDE ?? '') @@ -const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '') +const EXCLUDE_PATTERNS = (process.env.STATIC_PRELOAD_EXCLUDE ?? '') @@ -const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true' +const VERBOSE = process.env.STATIC_PRELOAD_VERBOSE === 'true' @@ -const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true' +const ENABLE_ETAG = (process.env.STATIC_PRELOAD_ETAG ?? 'true') === 'true' @@ -const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true' -const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) +const ENABLE_GZIP = (process.env.STATIC_PRELOAD_GZIP ?? 'true') === 'true' +const GZIP_MIN_BYTES = Number(process.env.STATIC_PRELOAD_GZIP_MIN_BYTES ?? 1024) const GZIP_TYPES = ( - process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ?? + process.env.STATIC_PRELOAD_GZIP_TYPES ?? @@ - const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '') + const raw = (process.env.STATIC_PRELOAD_INCLUDE ?? '')(Also update the verbose logging lines that print
Include patterns
/Exclude patterns
to read from theSTATIC_PRELOAD_*
envs.)
334-346
: Avoid immutable caching for HTML documentsPreloaded HTML is currently marked immutable, so clients and CDNs will sit on it for 1 year and never see fresh markup. Detect
text/html
and treat it as non-immutable before handing it tocreateResponseHandler
.if (matchesPattern && withinSizeLimit) { const bytes = new Uint8Array(await file.arrayBuffer()) const gz = compressDataIfAppropriate(bytes, metadata.type) const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined + const isHtml = metadata.type.startsWith('text/html') const asset: InMemoryAsset = { raw: bytes, gz, etag, type: metadata.type, - immutable: true, + immutable: !isHtml, size: bytes.byteLength, }
229-257
: AddVary: Accept-Encoding
alongside gzip negotiationBecause we sometimes serve gzip and sometimes plain bytes, every cacheable response (including 304) must vary on
Accept-Encoding
to avoid cache poisoning. Please attach the header once gzip is enabled and mirror it on the 304 response.const headers: Record<string, string> = { 'Content-Type': asset.type, 'Cache-Control': asset.immutable ? 'public, max-age=31536000, immutable' : 'public, max-age=3600', } + if (ENABLE_GZIP) { + headers['Vary'] = 'Accept-Encoding' + } @@ return new Response(null, { status: 304, - headers: { ETag: asset.etag }, + headers: ENABLE_GZIP + ? { ETag: asset.etag, 'Vary': 'Accept-Encoding' } + : { ETag: asset.etag }, })
"@tailwindcss/vite": "^4.1.13", | ||
"@tanstack/react-devtools": "^0.7.0", | ||
"@tanstack/react-router": "^1.132.7", | ||
"@tanstack/react-router-devtools": "^1.132.7", | ||
"@tanstack/react-router-ssr-query": "^1.132.7", | ||
"@tanstack/react-start": "^1.132.7", | ||
"@tanstack/router-plugin": "^1.132.7", | ||
"react": "^19.1.1", | ||
"react-dom": "^19.1.1", | ||
"tailwindcss": "^4.1.13", | ||
"vite-tsconfig-paths": "^5.1.4" | ||
}, | ||
"devDependencies": { | ||
"@tanstack/eslint-config": "^0.3.2", | ||
"@testing-library/dom": "^10.4.1", | ||
"@testing-library/react": "^16.3.0", | ||
"@types/bun": "^1.2.22", | ||
"@types/node": "22.10.2", | ||
"@types/react": "^19.1.13", | ||
"@types/react-dom": "^19.1.9", | ||
"@vitejs/plugin-react": "^5.0.3", | ||
"jsdom": "^27.0.0", | ||
"prettier": "^3.6.2", | ||
"typescript": "^5.9.2", | ||
"vite": "^7.1.7", | ||
"vitest": "^3.2.4", | ||
"web-vitals": "^5.1.0" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switch TanStack deps to workspace:*
These packages are part of this monorepo, so keeping caret ranges will bypass local builds and violate the repo guidelines for internal deps. Please swap them to workspace:*
so the example consumes the in-repo versions during development. As per coding guidelines.
"dependencies": {
- "@tanstack/react-devtools": "^0.7.0",
- "@tanstack/react-router": "^1.132.7",
- "@tanstack/react-router-devtools": "^1.132.7",
- "@tanstack/react-router-ssr-query": "^1.132.7",
- "@tanstack/react-start": "^1.132.7",
- "@tanstack/router-plugin": "^1.132.7",
+ "@tanstack/react-devtools": "workspace:*",
+ "@tanstack/react-router": "workspace:*",
+ "@tanstack/react-router-devtools": "workspace:*",
+ "@tanstack/react-router-ssr-query": "workspace:*",
+ "@tanstack/react-start": "workspace:*",
+ "@tanstack/router-plugin": "workspace:*",
@@
"devDependencies": {
- "@tanstack/eslint-config": "^0.3.2",
+ "@tanstack/eslint-config": "workspace:*",
📝 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.
"@tailwindcss/vite": "^4.1.13", | |
"@tanstack/react-devtools": "^0.7.0", | |
"@tanstack/react-router": "^1.132.7", | |
"@tanstack/react-router-devtools": "^1.132.7", | |
"@tanstack/react-router-ssr-query": "^1.132.7", | |
"@tanstack/react-start": "^1.132.7", | |
"@tanstack/router-plugin": "^1.132.7", | |
"react": "^19.1.1", | |
"react-dom": "^19.1.1", | |
"tailwindcss": "^4.1.13", | |
"vite-tsconfig-paths": "^5.1.4" | |
}, | |
"devDependencies": { | |
"@tanstack/eslint-config": "^0.3.2", | |
"@testing-library/dom": "^10.4.1", | |
"@testing-library/react": "^16.3.0", | |
"@types/bun": "^1.2.22", | |
"@types/node": "22.10.2", | |
"@types/react": "^19.1.13", | |
"@types/react-dom": "^19.1.9", | |
"@vitejs/plugin-react": "^5.0.3", | |
"jsdom": "^27.0.0", | |
"prettier": "^3.6.2", | |
"typescript": "^5.9.2", | |
"vite": "^7.1.7", | |
"vitest": "^3.2.4", | |
"web-vitals": "^5.1.0" | |
} | |
"dependencies": { | |
"@tailwindcss/vite": "^4.1.13", | |
"@tanstack/react-devtools": "workspace:*", | |
"@tanstack/react-router": "workspace:*", | |
"@tanstack/react-router-devtools": "workspace:*", | |
"@tanstack/react-router-ssr-query": "workspace:*", | |
"@tanstack/react-start": "workspace:*", | |
"@tanstack/router-plugin": "workspace:*", | |
"react": "^19.1.1", | |
"react-dom": "^19.1.1", | |
"tailwindcss": "^4.1.13", | |
"vite-tsconfig-paths": "^5.1.4" | |
}, | |
"devDependencies": { | |
"@tanstack/eslint-config": "workspace:*", | |
"@testing-library/dom": "^10.4.1", | |
"@testing-library/react": "^16.3.0", | |
"@types/bun": "^1.2.22", | |
"@types/node": "22.10.2", | |
"@types/react": "^19.1.13", | |
"@types/react-dom": "^19.1.9", | |
"@vitejs/plugin-react": "^5.0.3", | |
"jsdom": "^27.0.0", | |
"prettier": "^3.6.2", | |
"typescript": "^5.9.2", | |
"vite": "^7.1.7", | |
"vitest": "^3.2.4", | |
"web-vitals": "^5.1.0" | |
} |
🤖 Prompt for AI Agents
In examples/react/start-bun/package.json around lines 16 to 43, several
@tanstack packages are specified with caret version ranges; because these
packages live in the monorepo they must reference the local workspace builds
instead of external versions — replace the version strings for all @tanstack/*
entries in both "dependencies" and "devDependencies" (e.g.,
@tanstack/react-devtools, @tanstack/react-router,
@tanstack/react-router-devtools, @tanstack/react-router-ssr-query,
@tanstack/react-start, @tanstack/router-plugin, and @tanstack/eslint-config)
with "workspace:*" so the example consumes the in-repo packages during
development.
Description
This PR enhances the Bun deployment documentation for TanStack Start by adding information about a
production-ready server implementation.
Problem
The current documentation suggests running
bun run dist/server/server.js
after building, but thisapproach doesn't work properly with the current TanStack Start build output when using Bun as the
runtime.
Solution
Added documentation for an optimized production server (
server.ts
) that:What's included
Example Repository
A complete working example is available at: https://github.com/magnusmay/tanstack-start-bun-hosting
Summary by CodeRabbit
New Features
Documentation
Chores