From 27e85c741681a70a7a5d1a24b98e72df0e6e93c4 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:53:07 +0200 Subject: [PATCH 1/2] fix(tanstackstart-react): Rewrite source paths of split route modules TanStack Start splits route files into virtual modules such as `index.tsx?tss-serverfn-split`. The uploaded source maps kept these queries and paths relative to the map's directory, so frames did not show the path of the source file. Remove the split queries and make source paths relative to the project root, so frames show paths such as `src/routes/index.tsx`. A user's `sourcemaps.rewriteSources` still takes precedence. --- .../src/vite/sourceMaps.ts | 28 +++++++- .../test/vite/sourceMaps.test.ts | 67 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/sourceMaps.ts b/packages/tanstackstart-react/src/vite/sourceMaps.ts index 817cad432a22..113444db628a 100644 --- a/packages/tanstackstart-react/src/vite/sourceMaps.ts +++ b/packages/tanstackstart-react/src/vite/sourceMaps.ts @@ -1,9 +1,35 @@ import { sentryVitePlugin } from '@sentry/bundler-plugins/vite'; +import * as path from 'node:path'; import type { Plugin, UserConfig } from 'vite'; import type { SentryTanstackStartOptions } from './sentryTanstackStart'; type FilesToDeleteAfterUpload = string | string[] | undefined; +/** TanStack Router and Start split route files into virtual modules, e.g. `index.tsx?tss-serverfn-split`. */ +const TANSTACK_SPLIT_QUERY_REGEX = /\?ts[rs]-[^/]*$/; + +/** Sources such as `virtual:#nitro/runtime` are no file paths. Two letters at least, so `C:` stays a drive letter. */ +const SCHEME_REGEX = /^[a-z][a-z\d+.-]+:/i; + +/** + * Makes source paths relative to the project root and removes TanStack's split queries, so that client and server + * frames show the path of the source file, such as `src/routes/index.tsx`. + * + * Paths in a source map are relative to the map's directory, which the default hook ignores. + * + * @internal Only exported for testing. + */ +export function rewriteTanstackStartSource(source: string, _map: unknown, context?: { mapDir: string }): string { + const sourcePath = source.replace(TANSTACK_SPLIT_QUERY_REGEX, ''); + + if (SCHEME_REGEX.test(sourcePath)) { + return sourcePath; + } + + const absolutePath = path.resolve(context?.mapDir ?? process.cwd(), sourcePath); + return path.relative(process.cwd(), absolutePath).split(path.sep).join('/'); +} + /** * A Sentry plugin for adding the @sentry/bundler-plugins/vite to automatically upload source maps to Sentry. */ @@ -70,7 +96,7 @@ export function makeAddSentryVitePlugin(options: SentryTanstackStartOptions): Pl assets: sourcemaps?.assets, disable: sourcemaps?.disable, ignore: sourcemaps?.ignore, - rewriteSources: sourcemaps?.rewriteSources, + rewriteSources: sourcemaps?.rewriteSources ?? rewriteTanstackStartSource, resolveSourceMap: sourcemaps?.resolveSourceMap, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, }, diff --git a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts index e5ea14c95b3e..eb4b3740b658 100644 --- a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts +++ b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts @@ -2,11 +2,13 @@ import type { SentryVitePluginOptions } from '@sentry/bundler-plugins/vite'; import type { UserConfig } from 'vite'; import { mergeConfig } from 'vite'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as path from 'path'; import { getUpdatedSourceMapSettings, makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin, + rewriteTanstackStartSource, } from '../../src/vite/sourceMaps'; const mockedSentryVitePlugin = { @@ -273,6 +275,16 @@ describe('makeAddSentryVitePlugin()', () => { ); }); + it('rewrites sources with the TanStack Start hook by default', () => { + makeAddSentryVitePlugin({}); + + expect(sentryVitePluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ + sourcemaps: expect.objectContaining({ rewriteSources: rewriteTanstackStartSource }), + }), + ); + }); + it('sets the correct metaFramework in telemetry options', () => { makeAddSentryVitePlugin({ org: 'my-org', @@ -353,3 +365,56 @@ describe('getUpdatedSourceMapSettings', () => { expect(result).toBe('hidden'); }); }); + +describe('rewriteTanstackStartSource', () => { + const projectRoot = path.resolve('/projects/shop'); + + beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue(projectRoot); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.each([ + { + label: 'a server function split from a route', + source: '../../../src/routes/index.tsx?tss-serverfn-split', + mapDir: '.output/server/_ssr', + expected: 'src/routes/index.tsx', + }, + { + label: 'a route component split for the client', + source: '../../../src/routes/users.$userId.tsx?tsr-split=component', + mapDir: '.output/public/assets', + expected: 'src/routes/users.$userId.tsx', + }, + { + label: 'a dependency', + source: '../../node_modules/.pnpm/h3@2.0.2/node_modules/h3/dist/index.mjs', + mapDir: '.output/server', + expected: 'node_modules/.pnpm/h3@2.0.2/node_modules/h3/dist/index.mjs', + }, + { + label: 'a source that climbs fewer directories than the map is deep', + source: '../shared/format-price.ts', + mapDir: '.output/server/chunks/build', + expected: '.output/server/chunks/shared/format-price.ts', + }, + ])('makes the path of $label relative to the project root', ({ source, mapDir, expected }) => { + expect(rewriteTanstackStartSource(source, {}, { mapDir: path.join(projectRoot, mapDir) })).toBe(expected); + }); + + it('keeps sources that are no file paths', () => { + const mapDir = path.join(projectRoot, '.output/server'); + + expect(rewriteTanstackStartSource('virtual:#nitro/virtual/plugins', {}, { mapDir })).toBe( + 'virtual:#nitro/virtual/plugins', + ); + }); + + it('resolves sources from the project root without a map directory', () => { + expect(rewriteTanstackStartSource('src/routes/index.tsx?tss-serverfn-split', {})).toBe('src/routes/index.tsx'); + }); +}); From 33ee36432a7a8d54314f4b5f1c63517ef3b35da8 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:52:24 +0200 Subject: [PATCH 2/2] Update packages/tanstackstart-react/src/vite/sourceMaps.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Peer Stöcklmair --- packages/tanstackstart-react/src/vite/sourceMaps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tanstackstart-react/src/vite/sourceMaps.ts b/packages/tanstackstart-react/src/vite/sourceMaps.ts index 113444db628a..a0cf3f4e84c7 100644 --- a/packages/tanstackstart-react/src/vite/sourceMaps.ts +++ b/packages/tanstackstart-react/src/vite/sourceMaps.ts @@ -8,7 +8,7 @@ type FilesToDeleteAfterUpload = string | string[] | undefined; /** TanStack Router and Start split route files into virtual modules, e.g. `index.tsx?tss-serverfn-split`. */ const TANSTACK_SPLIT_QUERY_REGEX = /\?ts[rs]-[^/]*$/; -/** Sources such as `virtual:#nitro/runtime` are no file paths. Two letters at least, so `C:` stays a drive letter. */ +/** Sources such as `virtual:#nitro/runtime` are no file paths. Two letters at least, so drive letters, like `C:`, are not matched by the regex. */ const SCHEME_REGEX = /^[a-z][a-z\d+.-]+:/i; /**