Skip to content

Conversation

magnusmay
Copy link
Contributor

@magnusmay magnusmay commented Sep 25, 2025

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 this
approach 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:

  • ✅ Properly serves TanStack Start applications with Bun
  • ⚡ Implements intelligent static asset loading (preloads small files, serves large files on-demand)
  • 🎯 Provides configurable memory management via environment variables
  • 📊 Includes detailed logging for production monitoring
  • 🚀 Offers production-ready caching headers

What's included

  • Clear explanation that the default approach doesn't work with Bun
  • Step-by-step setup instructions
  • Configuration options via environment variables
  • Example configurations for different use cases
  • Link to working example repository

Example Repository

A complete working example is available at: https://github.com/magnusmay/tanstack-start-bun-hosting

Summary by CodeRabbit

  • New Features

    • Added a React + Bun example app with a Bun production server and demo routes (home, API, server-driven Todo, API-request), shared header, and Tailwind styles.
  • Documentation

    • Expanded Bun hosting docs into a full production workflow, switched install instructions to Bun, added environment-variable examples and sample server output; added README for the Bun example.
  • Chores

    • Added project configs (editor, lint, format, prettier), manifest/robots/gitignore; bumped Vite devDependency across many examples.

Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Docs: React hosting
docs/start/framework/react/hosting.md
Replace npm upgrade command with bun install react@19 react-dom@19, add punctuation, and add a full "Production Server with Bun" section documenting a custom Bun production server, env vars, setup, and sample output.
New example: project scaffolding & tooling
examples/react/start-bun/.gitignore, examples/react/start-bun/.prettierignore, examples/react/start-bun/.vscode/settings.json, examples/react/start-bun/README.md, examples/react/start-bun/eslint.config.js, examples/react/start-bun/package.json, examples/react/start-bun/prettier.config.js, examples/react/start-bun/tsconfig.json, examples/react/start-bun/vite.config.ts
Add a new React+Bn example with ignore rules, editor settings, README, ESLint/Prettier exports, package.json scripts/deps, tsconfig, and Vite config (default export).
Public assets
examples/react/start-bun/public/manifest.json, examples/react/start-bun/public/robots.txt
Add PWA manifest and default robots.txt.
Bun production server
examples/react/start-bun/server.ts
New Bun-based production server implementing env-driven hybrid asset loading (include/exclude globs, preload size threshold), in-memory preloading with optional gzip/ETag, on-demand disk serving, caching headers, verbose logging, dynamic import of TanStack Start handler, and route mounting.
Router & generated route tree
examples/react/start-bun/src/router.tsx, examples/react/start-bun/src/routeTree.gen.ts
Add getRouter factory and a typed generated routeTree with module augmentation, route type interfaces, runtime route exports, and Start SSR register typing.
Routes, components & styles
examples/react/start-bun/src/routes/__root.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.api-request.tsx, examples/react/start-bun/src/routes/demo.start.server-funcs.tsx, examples/react/start-bun/src/components/Header.tsx, examples/react/start-bun/src/styles.css
Add root layout, index page, an API route returning demo names, demo pages (API request and server functions with file-backed todos), a Header component, and Tailwind-based styles.
Generated runtime files
examples/react/start-bun/src/routeTree.gen.ts, examples/react/start-bun/src/router.tsx
Add generated route typings, runtime route exports, and router creator to wire example routes into TanStack Router/Start.
Vite version bumps across examples
Many **/package.json files (examples and e2e folders)
Upgrade devDependency vite from ^7.1.1 to ^7.1.7 (patch/minor bumps) across numerous example and e2e package.json files; a few examples also add @types/node.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add start-basic-cloudflare example #5254 — Adds React Start example scaffolding and generated routeTree/getRouter implementations in another example folder; strongly related to the generated route-tree and router additions here.

Suggested labels

package: react-start, package: start-client-core

Poem

I hopped through globs and cached a few,
Small bits in memory, big files queued, too.
Bun woke the routes and Start took the cart,
I gzip, I tag, I route every part.
A rabbit server — nimble, warm, and smart! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly communicates that this pull request adds documentation for a Bun production server to the TanStack Start project, which aligns precisely with the primary changes in the PR. It follows conventional commit style, uses clear and specific terms, and avoids unnecessary detail. A teammate reviewing the commit history will immediately understand the main purpose of this update.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the documentation Everything documentation related label Sep 25, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between ed7004b and afc5e2a.

📒 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 solid

The new section clearly walks through setup, configuration, and observability for the Bun server, which should make deployments much smoother.

@roderik
Copy link

roderik commented Sep 26, 2025

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!

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 accessibility

Add 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 build

Relative 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.json

Consider adding a start script in docs to mirror usage: "start": "bun run server.ts".


255-271: Clarify boolean env parsing

If 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.ts
examples/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/format

As 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 headers

Slightly 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 response

Add 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/devtools

A 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 builds

Avoid 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 filename

Project uses vite.config.ts, but the include lists vite.config.js.

-    "vite.config.js"
+    "vite.config.ts"

20-21: Prefer verbatimModuleSyntax with bundlers

Setting 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 lazily

Validate 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 file

Using 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 EISDIR

System errors expose code (e.g., 'EISDIR'), not via name.

-        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

📥 Commits

Reviewing files that changed from the base of the PR and between afc5e2a and 380d0c9.

⛔ 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 appropriate

Good DX guardrails for generated file.

examples/react/start-bun/prettier.config.js (1)

3-10: LGTM: valid Prettier ESM config with JSDoc typing

No issues.

examples/react/start-bun/eslint.config.js (1)

3-5: LGTM: Flat config export of TanStack ESLint preset

Looks 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 UI

Asset import and JSX look correct for Vite + React 19.

examples/react/start-bun/src/router.tsx (1)

7-13: Router factory looks good

Clean router creation with routeTree and sensible defaults.

examples/react/start-bun/vite.config.ts (1)

7-17: Vite plugin stack LGTM

Order 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 the meta array is supported by the Start head API. If not, move it to the top-level title field of the head config.

examples/react/start-bun/server.ts (1)

80-86: Regex from env input: acceptable here but keep it simple

You 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 consistent

The generated child bindings and module augmentations line up with the referenced routes and the getRouter contract. No issues spotted.

Comment on lines +1 to +5
import fs from 'node:fs'
import { useCallback, useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +8 to +21
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,
),
),
)
}
Copy link
Contributor

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.

Suggested change
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.

@notKamui
Copy link
Contributor

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)
})

@checkerschaf
Copy link

checkerschaf commented Sep 26, 2025

One thing I'm doing in my own version is serving static files that end with /index.html as a static route. This approach is useful for pre-rendered pages.

Example: /test/hello-world/index.html should be served as /test/hello-world.

Also I'm doing the same for top-level html files.

Example: /hello-world.html should be served as /hello-world

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.gzipSync

Passing 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” semantics

Docs 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’ handler

Be 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 guardrails

Current 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

📥 Commits

Reviewing files that changed from the base of the PR and between 380d0c9 and c6e0a95.

📒 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)

@jph-sw
Copy link

jph-sw commented Sep 26, 2025

Hello,

when running bun run server.ts and visiting the site, I can reach the server but in the network tab I'm getting a 404 response for /. Do you know, what this could be? I looked inside the dist/client and there aren't any .html files.

Copy link

nx-cloud bot commented Sep 26, 2025

View your CI Pipeline Execution ↗ for commit 70d0c75

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 5m 30s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 40s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-26 23:01:05 UTC

@schiller-manuel schiller-manuel force-pushed the docs/tanstack-start-bun-server branch from 81b7a95 to 4f6531e Compare September 26, 2025 20:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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_url

Small 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 risks

These 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 capabilities

Expose 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 clarity

Slightly 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 inference

Return 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

📥 Commits

Reviewing files that changed from the base of the PR and between 81b7a95 and 4f6531e.

⛔ 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 solid

Bringing 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 add

Prefer 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 -->

Comment on lines +248 to +250
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:
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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/.

Comment on lines 91 to 101
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));

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.

Comment on lines 211 to 223
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines 231 to 265
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 });
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines 336 to 349
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);
Copy link
Contributor

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.

Suggested change
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 filtering

Dynamic 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-values

Plain 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 emitted

If 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6531e and c14260b.

📒 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 bytes

Using 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 immutable

Immutable 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,
           }

Comment on lines +19 to +61
* 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
*
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.

Comment on lines +91 to +123
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'
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +229 to +246
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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

magnusmay and others added 4 commits September 26, 2025 23:07
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.
@schiller-manuel schiller-manuel force-pushed the docs/tanstack-start-bun-server branch from c14260b to e06b806 Compare September 26, 2025 21:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between c14260b and e06b806.

⛔ 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 URL

Linking 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.ts

As 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 expect ASSET_PRELOAD_*. As shipped, none of the documented env vars will be honored, so preloading/gzip/etag tuning silently fails. Please rename every remaining ASSET_PRELOAD_* reference (including the comment and env reads) to the matching STATIC_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, and GZIP_TYPES. Update the comment block accordingly.


229-257: Emit Vary: Accept-Encoding when negotiating gzip

These handlers serve different representations (plain vs gzip) based solely on Accept-Encoding, but the responses lack Vary: Accept-Encoding. Shared caches can therefore hand a gzipped payload to a client that didn’t request gzip (or vice versa). Add the Vary 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 immutable

Preloaded 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. Detect text/html when building asset and force immutable: 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,
           }

Comment on lines +1 to +35
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 (
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

@schiller-manuel schiller-manuel force-pushed the docs/tanstack-start-bun-server branch from e06b806 to 4e16af0 Compare September 26, 2025 21:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between e06b806 and 8abb867.

⛔ 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-bun

As 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_' -C1

Also 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,
           }

Copy link

pkg-pr-new bot commented Sep 26, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5237

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5237

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5237

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5237

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5237

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5237

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5237

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5237

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5237

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5237

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5237

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5237

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5237

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5237

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5237

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5237

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5237

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5237

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5237

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5237

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5237

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5237

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5237

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5237

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5237

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5237

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5237

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5237

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5237

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5237

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5237

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5237

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5237

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5237

commit: 70d0c75

@schiller-manuel schiller-manuel force-pushed the docs/tanstack-start-bun-server branch from 2f63984 to 3160df5 Compare September 26, 2025 22:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2f63984 and 70d0c75.

⛔ 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 documented STATIC_PRELOAD_* keys

The README (and hosting docs) instruct users to set STATIC_PRELOAD_*, but the server still reads ASSET_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 the STATIC_PRELOAD_* envs.)


334-346: Avoid immutable caching for HTML documents

Preloaded 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 to createResponseHandler.

         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: Add Vary: Accept-Encoding alongside gzip negotiation

Because 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 },
         })

Comment on lines +16 to +43
"@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"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
"@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.

@schiller-manuel schiller-manuel merged commit 2ff5bc2 into TanStack:main Sep 26, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants