feat(core): Expose rewriteSources top level option #20142
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨Cloudflare
Core
Deps
Other
Bug Fixes 🐛Deno
Other
Internal Changes 🔧Ci
Deps
Deps Dev
Other
🤖 This preview updates automatically when you update the PR. |
size-limit report 📦
|
node-overhead report 🧳Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
|
Lms24
left a comment
There was a problem hiding this comment.
Thanks for adding this!
Just to confirm: We already bumped the plugins to the supported version? And sveltekit uses the interface from core directly?
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: No integration or E2E test for feature
- Added integration tests in nextjs, nuxt, and tanstackstart-react packages that verify user-provided rewriteSources hooks are invoked with proper arguments and return correct results during the source map configuration flow.
Or push these changes by commenting:
@cursor push 0f5c31dc99
Preview (0f5c31dc99)
diff --git a/packages/nextjs/test/config/rewriteSources.integration.test.ts b/packages/nextjs/test/config/rewriteSources.integration.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/nextjs/test/config/rewriteSources.integration.test.ts
@@ -1,0 +1,118 @@
+import { describe, it, expect, vi } from 'vitest';
+import { getBuildPluginOptions } from '../../src/config/getBuildPluginOptions';
+import type { SentryBuildOptions } from '../../src/config/types';
+
+const mockReleaseName = 'test-release';
+const mockDistDirAbsPath = '/test/project/.next';
+
+describe('rewriteSources integration', () => {
+ it('invokes custom rewriteSources function with source paths', () => {
+ const rewriteSourcesSpy = vi.fn((source: string) => {
+ return source.replace(/^custom\//, 'rewritten/');
+ });
+
+ const sentryBuildOptions: SentryBuildOptions = {
+ org: 'test-org',
+ project: 'test-project',
+ sourcemaps: {
+ rewriteSources: rewriteSourcesSpy,
+ },
+ };
+
+ const result = getBuildPluginOptions({
+ sentryBuildOptions,
+ releaseName: mockReleaseName,
+ distDirAbsPath: mockDistDirAbsPath,
+ buildTool: 'webpack-client',
+ });
+
+ const rewriteSources = result.sourcemaps?.rewriteSources;
+ expect(rewriteSources).toBeDefined();
+ expect(rewriteSources).toBe(rewriteSourcesSpy);
+
+ const testPaths = [
+ 'custom/src/pages/index.js',
+ 'custom/components/Button.tsx',
+ 'src/utils/helpers.js',
+ 'custom/lib/api.ts',
+ ];
+
+ testPaths.forEach(path => {
+ rewriteSources?.(path, {});
+ });
+
+ expect(rewriteSourcesSpy).toHaveBeenCalledTimes(4);
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(1, 'custom/src/pages/index.js', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(2, 'custom/components/Button.tsx', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(3, 'src/utils/helpers.js', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(4, 'custom/lib/api.ts', {});
+
+ expect(rewriteSourcesSpy.mock.results[0]?.value).toBe('rewritten/src/pages/index.js');
+ expect(rewriteSourcesSpy.mock.results[1]?.value).toBe('rewritten/components/Button.tsx');
+ expect(rewriteSourcesSpy.mock.results[2]?.value).toBe('src/utils/helpers.js');
+ expect(rewriteSourcesSpy.mock.results[3]?.value).toBe('rewritten/lib/api.ts');
+ });
+
+ it('invokes default rewriteSources function for webpack sources', () => {
+ const sentryBuildOptions: SentryBuildOptions = {
+ org: 'test-org',
+ project: 'test-project',
+ };
+
+ const result = getBuildPluginOptions({
+ sentryBuildOptions,
+ releaseName: mockReleaseName,
+ distDirAbsPath: mockDistDirAbsPath,
+ buildTool: 'webpack-client',
+ });
+
+ const rewriteSources = result.sourcemaps?.rewriteSources;
+ expect(rewriteSources).toBeDefined();
+
+ const webpackPaths = [
+ 'webpack://_N_E/src/pages/index.js',
+ 'webpack://project/src/components/Button.js',
+ 'src/utils/helpers.js',
+ 'webpack://_N_E/./app/layout.tsx',
+ ];
+
+ const rewrittenPaths = webpackPaths.map(path => rewriteSources?.(path, {}));
+
+ expect(rewrittenPaths).toEqual([
+ 'src/pages/index.js',
+ 'project/src/components/Button.js',
+ 'src/utils/helpers.js',
+ './app/layout.tsx',
+ ]);
+ });
+
+ it('preserves rewriteSources function reference across multiple builds', () => {
+ const customRewrite = (source: string) => source.replace(/^prefix\//, '');
+
+ const sentryBuildOptions: SentryBuildOptions = {
+ org: 'test-org',
+ project: 'test-project',
+ sourcemaps: {
+ rewriteSources: customRewrite,
+ },
+ };
+
+ const clientResult = getBuildPluginOptions({
+ sentryBuildOptions,
+ releaseName: mockReleaseName,
+ distDirAbsPath: mockDistDirAbsPath,
+ buildTool: 'webpack-client',
+ });
+
+ const serverResult = getBuildPluginOptions({
+ sentryBuildOptions,
+ releaseName: mockReleaseName,
+ distDirAbsPath: mockDistDirAbsPath,
+ buildTool: 'webpack-nodejs',
+ });
+
+ expect(clientResult.sourcemaps?.rewriteSources).toBe(customRewrite);
+ expect(serverResult.sourcemaps?.rewriteSources).toBe(customRewrite);
+ expect(clientResult.sourcemaps?.rewriteSources).toBe(serverResult.sourcemaps?.rewriteSources);
+ });
+});
diff --git a/packages/nuxt/test/vite/rewriteSources.integration.test.ts b/packages/nuxt/test/vite/rewriteSources.integration.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/nuxt/test/vite/rewriteSources.integration.test.ts
@@ -1,0 +1,72 @@
+import { describe, it, expect, vi } from 'vitest';
+import { getPluginOptions } from '../../src/vite/sourceMaps';
+import type { SentryNuxtModuleOptions } from '../../src/common/types';
+
+describe('rewriteSources integration', () => {
+ it('invokes custom rewriteSources function with source paths', () => {
+ const rewriteSourcesSpy = vi.fn((source: string) => {
+ return source.replace(/^src\//, 'custom/');
+ });
+
+ const options = getPluginOptions({
+ sourcemaps: {
+ rewriteSources: rewriteSourcesSpy,
+ },
+ } as SentryNuxtModuleOptions);
+
+ const rewriteSources = options.sourcemaps?.rewriteSources;
+ expect(rewriteSources).toBeDefined();
+ expect(rewriteSources).toBe(rewriteSourcesSpy);
+
+ const testPaths = [
+ 'src/pages/index.vue',
+ 'src/components/Button.vue',
+ 'lib/utils/helpers.ts',
+ 'src/composables/useAuth.ts',
+ ];
+
+ testPaths.forEach(path => {
+ rewriteSources?.(path);
+ });
+
+ expect(rewriteSourcesSpy).toHaveBeenCalledTimes(4);
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(1, 'src/pages/index.vue');
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(2, 'src/components/Button.vue');
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(3, 'lib/utils/helpers.ts');
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(4, 'src/composables/useAuth.ts');
+
+ expect(rewriteSourcesSpy.mock.results[0]?.value).toBe('custom/pages/index.vue');
+ expect(rewriteSourcesSpy.mock.results[1]?.value).toBe('custom/components/Button.vue');
+ expect(rewriteSourcesSpy.mock.results[2]?.value).toBe('lib/utils/helpers.ts');
+ expect(rewriteSourcesSpy.mock.results[3]?.value).toBe('custom/composables/useAuth.ts');
+ });
+
+ it('invokes default rewriteSources function to normalize paths', () => {
+ const options = getPluginOptions({} as SentryNuxtModuleOptions);
+
+ const rewriteSources = options.sourcemaps?.rewriteSources;
+ expect(rewriteSources).toBeDefined();
+
+ const paths = ['../../../foo/bar', '../../components/Layout.vue', './local', '../utils/api.ts'];
+
+ const rewrittenPaths = paths.map(path => rewriteSources?.(path));
+
+ expect(rewrittenPaths).toEqual(['./foo/bar', './components/Layout.vue', './local', './utils/api.ts']);
+ });
+
+ it('preserves rewriteSources function reference across configuration', () => {
+ const customRewrite = (source: string) => source.replace(/^prefix\//, '');
+
+ const options = getPluginOptions({
+ sourcemaps: {
+ rewriteSources: customRewrite,
+ },
+ } as SentryNuxtModuleOptions);
+
+ expect(options.sourcemaps?.rewriteSources).toBe(customRewrite);
+
+ const testSource = 'prefix/src/app.vue';
+ const result = options.sourcemaps?.rewriteSources?.(testSource);
+ expect(result).toBe('src/app.vue');
+ });
+});
diff --git a/packages/tanstackstart-react/test/vite/rewriteSources.integration.test.ts b/packages/tanstackstart-react/test/vite/rewriteSources.integration.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/tanstackstart-react/test/vite/rewriteSources.integration.test.ts
@@ -1,0 +1,83 @@
+import { describe, it, expect, vi } from 'vitest';
+
+const sentryVitePluginSpy = vi.fn(() => []);
+
+vi.mock('@sentry/vite-plugin', () => ({
+ sentryVitePlugin: vi.fn(() => []),
+}));
+
+import { makeAddSentryVitePlugin } from '../../src/vite/sourceMaps';
+import { sentryVitePlugin } from '@sentry/vite-plugin';
+
+vi.mocked(sentryVitePlugin).mockImplementation(sentryVitePluginSpy);
+
+describe('rewriteSources integration', () => {
+ it('invokes custom rewriteSources function with source paths', () => {
+ const rewriteSourcesSpy = vi.fn((source: string) => {
+ return source.replace(/^src\//, '');
+ });
+
+ makeAddSentryVitePlugin({
+ org: 'my-org',
+ authToken: 'my-token',
+ sourcemaps: {
+ rewriteSources: rewriteSourcesSpy,
+ },
+ });
+
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sourcemaps: expect.objectContaining({
+ rewriteSources: rewriteSourcesSpy,
+ }),
+ }),
+ );
+
+ const passedOptions = sentryVitePluginSpy.mock.calls[0]?.[0];
+ const rewriteSources = passedOptions?.sourcemaps?.rewriteSources;
+
+ expect(rewriteSources).toBe(rewriteSourcesSpy);
+
+ const testPaths = ['src/routes/index.tsx', 'src/components/Button.tsx', 'lib/utils.ts', 'src/api/client.ts'];
+
+ testPaths.forEach(path => {
+ rewriteSources?.(path, {});
+ });
+
+ expect(rewriteSourcesSpy).toHaveBeenCalledTimes(4);
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(1, 'src/routes/index.tsx', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(2, 'src/components/Button.tsx', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(3, 'lib/utils.ts', {});
+ expect(rewriteSourcesSpy).toHaveBeenNthCalledWith(4, 'src/api/client.ts', {});
+
+ expect(rewriteSourcesSpy.mock.results[0]?.value).toBe('routes/index.tsx');
+ expect(rewriteSourcesSpy.mock.results[1]?.value).toBe('components/Button.tsx');
+ expect(rewriteSourcesSpy.mock.results[2]?.value).toBe('lib/utils.ts');
+ expect(rewriteSourcesSpy.mock.results[3]?.value).toBe('api/client.ts');
+ });
+
+ it('preserves rewriteSources function reference in plugin options', () => {
+ const customRewrite = (source: string) => source.replace(/^prefix\//, '');
+
+ sentryVitePluginSpy.mockClear();
+
+ makeAddSentryVitePlugin({
+ org: 'my-org',
+ authToken: 'my-token',
+ sourcemaps: {
+ rewriteSources: customRewrite,
+ },
+ });
+
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sourcemaps: expect.objectContaining({
+ rewriteSources: customRewrite,
+ }),
+ }),
+ );
+
+ const passedOptions = sentryVitePluginSpy.mock.calls[0]?.[0];
+ expect(passedOptions?.sourcemaps?.rewriteSources).toBe(customRewrite);
+ });
+});This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2722abb. Configure here.


rewriteSourcesto the baseSourceMapsOptionsinterface so users can customize source path rewriting without theunstable_*escape hatchbundler plugins ref getsentry/sentry-javascript-bundler-plugins#908
closes #20028