Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/inject-source-order-pre.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools-vite': patch
---

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.
6 changes: 6 additions & 0 deletions packages/devtools-vite/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
return config.mode === 'development' && injectSourceConfig.enabled
},
transform: {
// Run before the transforms of plugins without `order: 'pre'`, also when
// devtools() is not first in the plugins array. A plugin that changes
// the code of one environment only would shift the lines and make the
// SSR and client locations differ, which breaks hydration. An earlier
// plugin whose transform also has `order: 'pre'` still runs first.
order: 'pre',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
filter: {
id: {
exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//],
Expand Down
62 changes: 62 additions & 0 deletions packages/devtools-vite/tests/inject-source-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @vitest-environment node
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createServer } from 'vite'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { devtools } from '../src/plugin'
import type { ViteDevServer } from 'vite'

describe('inject-source order', () => {
let root: string
let server: ViteDevServer

beforeAll(async () => {
root = mkdtempSync(join(tmpdir(), 'tsd-inject-order-'))
writeFileSync(
join(root, 'App.tsx'),
'export function App() {\n return <div>hi</div>\n}\n',
)
server = await createServer({
root,
mode: 'development',
configFile: false,
logLevel: 'silent',
server: { middlewareMode: true, ws: false },
// The classic runtime needs no react import, so the fixture needs no
// node_modules.
oxc: { jsx: { runtime: 'classic' } },
plugins: [
// A framework plugin listed before devtools() that changes the code of
// one environment only, like a server-function compiler.
{
name: 'shift-ssr-lines',
enforce: 'pre',
transform(code, id, options) {
return options?.ssr && id.endsWith('.tsx')
? `\n\n\n${code}`
: undefined
},
},
devtools({
eventBusConfig: { enabled: false },
consolePiping: { enabled: false },
}),
],
})
})

afterAll(async () => {
await server?.close()
rmSync(root, { recursive: true, force: true })
})

it('injects the same source location on the client and the server', async () => {
const client = await server.transformRequest('/App.tsx')
const ssr = await server.transformRequest('/App.tsx', { ssr: true })
const sourceOf = (code = '') => code.match(/App\.tsx:\d+:\d+/)?.[0]

expect(sourceOf(client?.code)).toBe('App.tsx:2:10')
expect(sourceOf(ssr?.code)).toBe('App.tsx:2:10')
})
})
Loading