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.
- Adds `rewriteSources` to the base `SourceMapsOptions` interface so users can customize source path rewriting without the `unstable_*` escape hatch - Wires up the option in Nuxt, Next.js, and TanStack Start (SvelteKit, Astro, React Router already pass it through via spread) - Nuxt and Next.js preserve their default rewriting behavior when the option is not provided bundler plugins ref getsentry/sentry-javascript-bundler-plugins#908 closes #20028


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