Skip to content
Merged
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
28 changes: 27 additions & 1 deletion packages/tanstackstart-react/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -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 drive letters, like `C:`, are not matched by the regex. */
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.
*/
Expand Down Expand Up @@ -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,
},
Expand Down
67 changes: 66 additions & 1 deletion packages/tanstackstart-react/test/vite/sourceMaps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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');
});
});
Loading