Skip to content

Commit 40f32e4

Browse files
authored
fix(devtools-vite): inject data-tsd-source before other plugins transform (#548)
* fix(devtools-vite): inject data-tsd-source before other plugins transform inject-source uses `enforce: 'pre'`, so a `pre` plugin listed before devtools() transformed the code first. When that plugin changes the code of one environment only (a server-function compiler, for example), the lines shift, and data-tsd-source differs between the SSR and client output. React then reports a hydration mismatch, for example `editor.tsx:632:5` on the server and `editor.tsx:636:5` on the client. The transform hook now has `order: 'pre'`, so it runs before the other plugins' transform hooks, whatever their position in the array. Fixes #405 Fixes #189 * docs(devtools-vite): note that an earlier order: 'pre' transform still runs first
1 parent 0803d09 commit 40f32e4

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools-vite': patch
3+
---
4+
5+
Run source injection before the transforms of other plugins (unless they also use `order: 'pre'`), also when `devtools()` is not first in the plugins array. Before this fix, a framework plugin that changed the code of only one environment shifted the line numbers, so `data-tsd-source` differed between SSR and client and caused hydration warnings.

‎packages/devtools-vite/src/plugin.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
6060
return config.mode === 'development' && injectSourceConfig.enabled
6161
},
6262
transform: {
63+
// Run before the transforms of plugins without `order: 'pre'`, also when
64+
// devtools() is not first in the plugins array. A plugin that changes
65+
// the code of one environment only would shift the lines and make the
66+
// SSR and client locations differ, which breaks hydration. An earlier
67+
// plugin whose transform also has `order: 'pre'` still runs first.
68+
order: 'pre',
6369
filter: {
6470
id: {
6571
exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//],
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// @vitest-environment node
2+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { createServer } from 'vite'
6+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
7+
import { devtools } from '../src/plugin'
8+
import type { ViteDevServer } from 'vite'
9+
10+
describe('inject-source order', () => {
11+
let root: string
12+
let server: ViteDevServer
13+
14+
beforeAll(async () => {
15+
root = mkdtempSync(join(tmpdir(), 'tsd-inject-order-'))
16+
writeFileSync(
17+
join(root, 'App.tsx'),
18+
'export function App() {\n return <div>hi</div>\n}\n',
19+
)
20+
server = await createServer({
21+
root,
22+
mode: 'development',
23+
configFile: false,
24+
logLevel: 'silent',
25+
server: { middlewareMode: true, ws: false },
26+
// The classic runtime needs no react import, so the fixture needs no
27+
// node_modules.
28+
oxc: { jsx: { runtime: 'classic' } },
29+
plugins: [
30+
// A framework plugin listed before devtools() that changes the code of
31+
// one environment only, like a server-function compiler.
32+
{
33+
name: 'shift-ssr-lines',
34+
enforce: 'pre',
35+
transform(code, id, options) {
36+
return options?.ssr && id.endsWith('.tsx')
37+
? `\n\n\n${code}`
38+
: undefined
39+
},
40+
},
41+
devtools({
42+
eventBusConfig: { enabled: false },
43+
consolePiping: { enabled: false },
44+
}),
45+
],
46+
})
47+
})
48+
49+
afterAll(async () => {
50+
await server?.close()
51+
rmSync(root, { recursive: true, force: true })
52+
})
53+
54+
it('injects the same source location on the client and the server', async () => {
55+
const client = await server.transformRequest('/App.tsx')
56+
const ssr = await server.transformRequest('/App.tsx', { ssr: true })
57+
const sourceOf = (code = '') => code.match(/App\.tsx:\d+:\d+/)?.[0]
58+
59+
expect(sourceOf(client?.code)).toBe('App.tsx:2:10')
60+
expect(sourceOf(ssr?.code)).toBe('App.tsx:2:10')
61+
})
62+
})

0 commit comments

Comments
 (0)