From 4f63886e45f217a1377d7209cefc4f349d47bfad Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 16:03:12 +0200 Subject: [PATCH 1/2] 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 --- .changeset/inject-source-order-pre.md | 5 ++ packages/devtools-vite/src/plugin.ts | 5 ++ .../tests/inject-source-order.test.ts | 62 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .changeset/inject-source-order-pre.md create mode 100644 packages/devtools-vite/tests/inject-source-order.test.ts diff --git a/.changeset/inject-source-order-pre.md b/.changeset/inject-source-order-pre.md new file mode 100644 index 000000000..a99f6f623 --- /dev/null +++ b/.changeset/inject-source-order-pre.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-vite': patch +--- + +Run source injection before the transforms of other plugins, 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. diff --git a/packages/devtools-vite/src/plugin.ts b/packages/devtools-vite/src/plugin.ts index 8c7acdd65..6db781e67 100644 --- a/packages/devtools-vite/src/plugin.ts +++ b/packages/devtools-vite/src/plugin.ts @@ -60,6 +60,11 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { return config.mode === 'development' && injectSourceConfig.enabled }, transform: { + // Run before the other plugins' transforms, 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. + order: 'pre', filter: { id: { exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//], diff --git a/packages/devtools-vite/tests/inject-source-order.test.ts b/packages/devtools-vite/tests/inject-source-order.test.ts new file mode 100644 index 000000000..67dce0913 --- /dev/null +++ b/packages/devtools-vite/tests/inject-source-order.test.ts @@ -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
hi
\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') + }) +}) From 39d3355db5b9e634da13093d9436c8651f2025c4 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 17:36:02 +0200 Subject: [PATCH 2/2] docs(devtools-vite): note that an earlier order: 'pre' transform still runs first --- .changeset/inject-source-order-pre.md | 2 +- packages/devtools-vite/src/plugin.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.changeset/inject-source-order-pre.md b/.changeset/inject-source-order-pre.md index a99f6f623..372a1f6fd 100644 --- a/.changeset/inject-source-order-pre.md +++ b/.changeset/inject-source-order-pre.md @@ -2,4 +2,4 @@ '@tanstack/devtools-vite': patch --- -Run source injection before the transforms of other plugins, 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. +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. diff --git a/packages/devtools-vite/src/plugin.ts b/packages/devtools-vite/src/plugin.ts index 6db781e67..fd7260187 100644 --- a/packages/devtools-vite/src/plugin.ts +++ b/packages/devtools-vite/src/plugin.ts @@ -60,10 +60,11 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { return config.mode === 'development' && injectSourceConfig.enabled }, transform: { - // Run before the other plugins' transforms, 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. + // 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', filter: { id: {