Repository: TanStack/router
Pull request #2956 (2024-12-08 21:52:45)

fix(start): Fix json() helper merging of headers, and set up unit tests for @tanstack/start
This PR was originally meant to just be an improvement for the json() helper, but in the process of writing a unit test for it, I realised that unit tests in the @tanstack/start package were not being run.

The existing headers merging wouldn't work correctly for arrays of entries, or Headers instances, both of which were allowed by the types. This PR fixes that by contructing a Headers instance ourselves, *then* adding the Content-Type header.

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 625a8687d34acdba920d5756b85bad1999d2e5aa.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/6756154320042e40ba338a45?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/4eb5ad25-6e36-45b4-9013-950f51543720)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/9112341d-b3f3-429c-be6b-2fb820589629)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/56373d7b-3707-46e8-98f5-41fcc9e40144)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/19e4d91c-1185-4356-bd8a-ffebb69ca9f4)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/35902a20-95d0-49fd-a705-67f42ef13450)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/0b7ad5ba-515c-4084-a810-8cae58d1cfe2)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/dc0c2ddb-ebb9-44e6-84c2-b992b12139e5)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/0dbb09ea-6b1c-4a22-9e7f-b9133eb172dc)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/82a05bc8-88c2-4892-bd2a-9e5d764018db)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/f33201e3-e834-4ba5-a7af-110f88548d83)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/f5de4f2e-a2e4-490c-93d6-02f5ffd7560b)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/eed81395-a02b-446c-8a95-31472a990978)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/77c6f23b-eb34-485b-9d1e-1ed16c79eeaa)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/7a447433-60a3-44e9-ad8c-3c0677c94a30)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/a56f807c-eb2f-4390-b757-6f0ec167db82)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/6b4454b5-c65a-4e1e-8fcd-6f6771d03906)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/ce93fbae-8723-40fe-859a-9166fdf910f9)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/71c07de7-1e88-4cb3-8f5d-f7b24042368e)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/d0a773d9-7768-48bd-a09f-4001438bd058)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/64a58ae8-5ac4-4b16-8adf-087e921817ae)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/89a6f803-4886-494f-9f43-f36abe7ac553)
- [tanstack-router-react-example-quickstart-esbuild-file-based](https://pkg.pr.new/template/d6e75c64-4f11-45ae-af63-13e37b7cd9a1)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/fe16c487-e771-4fcd-a534-8475f503803b)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/c8f30135-7733-447c-bc0b-32d5313a4848)
- [router-monorepo-react-query](https://pkg.pr.new/template/1f74db8b-93f9-4dd6-ab5d-0f3a634a0b2a)
- [router-mono-simple](https://pkg.pr.new/template/ce45f912-1481-4f27-8dd0-0418d3a95efd)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/b3e13ebd-935e-4cbf-ae44-e81d54fb9ab5)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/c8ccf536-5b99-4083-bd5f-da9bb5177109)
- [tanstack-start-example-basic](https://pkg.pr.new/template/d48dc6e5-f422-46f9-b9cc-dfca91a2fe19)
- [tanstack-search-validator-adapters](https://pkg.pr.new/template/f6306979-6cf2-4a22-a9c4-36958dfb74e5)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/525daacd-c72a-4182-9ba7-625c614e51b4)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/5f4bd518-e0d4-464f-8399-0dccf6f4fc2f)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/559ac09d-1e29-4628-b223-6c07483e455a)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/9a83def1-ad2f-493d-ac03-68889a46d8ea)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/322335ab-3700-4297-ad40-9d9db24e095f)
- [tanstack-start-example-counter](https://pkg.pr.new/template/907cae51-3c1a-4c1c-aa2f-0de95253f7c7)
- [tanstack-start-example-large](https://pkg.pr.new/template/dfb4b7db-80b9-46bc-b58b-b87ba20c6189)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/9a51f161-7b45-4d45-a067-d12c8b6b9ba6)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/1437e22e-78eb-4880-9c24-0cda84039669)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/5115b3e6-7ad7-4d0d-a31f-97d474dde85e)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/73ff2402-10c9-4758-84ab-944ba4752ef7)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/95e1fe3e-9722-4d60-8cab-6ad8e4b8266f)

</p></details>
      
    


<details><summary><b>@tanstack/arktype-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/arktype-adapter@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/history</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/history@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-cross-context@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/create-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/create-router@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/eslint-plugin-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router-with-query@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-cli@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-devtools@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-generator@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-plugin@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/valibot-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/valibot-adapter@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/virtual-file-routes</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/virtual-file-routes@2956
```

</p></details>
      
    

<details><summary><b>@tanstack/zod-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/zod-adapter@2956
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/34099695279"><code>625a868</code></a>_


Patch:
diff --git a/packages/start/package.json b/packages/start/package.json
index 31eb6fb5f..20245a698 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -25,6 +25,8 @@
   ],
   "scripts": {
     "clean": "rimraf ./dist && rimraf ./coverage",
+    "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
+    "test:unit": "vitest",
     "test:eslint": "eslint ./src",
     "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
     "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
diff --git a/packages/start/src/client/json.ts b/packages/start/src/client/json.ts
index 356e55885..166199621 100644
--- a/packages/start/src/client/json.ts
+++ b/packages/start/src/client/json.ts
@@ -2,21 +2,15 @@ import type { JsonResponse } from './createServerFn'
 
 export function json<TData>(
   payload: TData,
-  opts?: {
-    status?: number
-    statusText?: string
-    headers?: HeadersInit
-  },
+  init?: ResponseInit,
 ): JsonResponse<TData> {
-  const status = opts?.status || 200
-  const statusText = opts?.statusText
+  const headers = new Headers(init?.headers)
+
+  if (!headers.has('Content-Type'))
+    headers.set('Content-Type', 'application/json')
 
   return new Response(JSON.stringify(payload), {
-    status,
-    statusText,
-    headers: {
-      'Content-Type': 'application/json',
-      ...opts?.headers,
-    },
+    ...init,
+    headers,
   })
 }
diff --git a/packages/start/vite.config.ts b/packages/start/vite.config.ts
index 4422860a9..82a14ae09 100644
--- a/packages/start/vite.config.ts
+++ b/packages/start/vite.config.ts
@@ -1,10 +1,16 @@
 import { defineConfig, mergeConfig } from 'vitest/config'
 import { tanstackViteConfig } from '@tanstack/config/vite'
 import react from '@vitejs/plugin-react'
-import type { UserConfig } from 'vitest/config'
+import packageJson from './package.json'
+import type { ViteUserConfig } from 'vitest/config'
 
 const config = defineConfig({
-  plugins: [react()] as UserConfig['plugins'],
+  plugins: [react()] as ViteUserConfig['plugins'],
+  test: {
+    name: packageJson.name,
+    watch: false,
+    environment: 'jsdom',
+  },
 })
 
 export default mergeConfig(


Test patch:
diff --git a/packages/start/src/client/tests/index.test.tsx b/packages/start/src/client/tests/index.test.tsx
index 9b64e1f92..e1757cc88 100644
--- a/packages/start/src/client/tests/index.test.tsx
+++ b/packages/start/src/client/tests/index.test.tsx
@@ -69,11 +69,7 @@ describe('ssr scripts', () => {
     const { container } = render(<RouterProvider router={router} />)
 
     expect(container.innerHTML).toEqual(
-      `<script id="__TSR_DEHYDRATED__">
-          __TSR_DEHYDRATED__ = {
-            data: '{}'
-          }
-        </script><script src="script.js"></script><script src="script2.js"></script><script src="script3.js"></script>`,
+      `<script src="script.js"></script><script src="script2.js"></script><script src="script3.js"></script>`,
     )
   })
 })
diff --git a/packages/start/src/client/tests/json.test.ts b/packages/start/src/client/tests/json.test.ts
new file mode 100644
index 000000000..fb9d8ac8f
--- /dev/null
+++ b/packages/start/src/client/tests/json.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from 'vitest'
+import { json } from '../json'
+
+describe('json', () => {
+  it('sets the content type to application/json and stringifies the data', async () => {
+    const data = { foo: 'bar' }
+    const response = json(data)
+
+    expect(response.headers.get('Content-Type')).toBe('application/json')
+
+    const responseClone = response.clone()
+    await expect(responseClone.text()).resolves.toEqual(JSON.stringify(data))
+
+    await expect(response.json()).resolves.toEqual(data)
+  })
+  it("doesn't override the content type if it's already set", () => {
+    const response = json(null, { headers: { 'Content-Type': 'text/plain' } })
+
+    expect(response.headers.get('Content-Type')).toBe('text/plain')
+  })
+  it('reflects passed status and statusText', () => {
+    const response = json(null, { status: 404, statusText: 'Not Found' })
+
+    expect(response.status).toBe(404)
+    expect(response.statusText).toBe('Not Found')
+  })
+  it.each<[string, HeadersInit]>([
+    ['plain object', { 'X-TYPE': 'example' }],
+    ['array', [['X-TYPE', 'example']]],
+    ['Headers', new Headers({ 'X-TYPE': 'example' })],
+  ])('merges headers from %s', (_, headers) => {
+    const response = json(null, { headers })
+
+    expect(response.headers.get('X-TYPE')).toBe('example')
+    expect(response.headers.get('Content-Type')).toBe('application/json')
+  })
+})