From ad05299342708cc9c14950592a406665e04af202 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 15 Sep 2025 18:22:49 +0200 Subject: [PATCH 01/21] unify buildPluginOptions --- .../src/config/getBuildPluginOptions.ts | 126 +++++++++++++++--- .../config/handleRunAfterProductionCompile.ts | 28 ++-- packages/nextjs/src/config/webpack.ts | 16 ++- .../nextjs/src/config/webpackPluginOptions.ts | 126 ------------------ 4 files changed, 133 insertions(+), 163 deletions(-) delete mode 100644 packages/nextjs/src/config/webpackPluginOptions.ts diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 3dfef3bbad08..9f1061925fd9 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -1,22 +1,28 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin-core'; import * as path from 'path'; -import type { SentryBuildOptions } from './types'; +import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types'; /** - * Get Sentry Build Plugin options for the runAfterProductionCompile hook. + * Get Sentry Build Plugin options for both webpack and turbopack builds. + * These options can be used in two ways: + * 1. The build can be done in a single operation after the production build completes + * 2. The build can be done in multiple operations, one for each webpack build */ export function getBuildPluginOptions({ sentryBuildOptions, releaseName, distDirAbsPath, + buildTool, + useRunAfterProductionCompileHook, }: { sentryBuildOptions: SentryBuildOptions; releaseName: string | undefined; distDirAbsPath: string; + buildTool: 'webpack-client' | 'webpack-nodejs' | 'webpack-edge' | 'after-production-compile'; + useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook }): SentryBuildPluginOptions { const sourcemapUploadAssets: string[] = []; const sourcemapUploadIgnore: string[] = []; - const filesToDeleteAfterUpload: string[] = []; // We need to convert paths to posix because Glob patterns use `\` to escape @@ -24,17 +30,74 @@ export function getBuildPluginOptions({ // See: https://www.npmjs.com/package/glob const normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/'); - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output - ); - if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { - filesToDeleteAfterUpload.push( - path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'), + const loggerPrefix = { + 'webpack-nodejs': '[@sentry/nextjs - Node.js]', + 'webpack-edge': '[@sentry/nextjs - Edge]', + 'webpack-client': '[@sentry/nextjs - Client]', + 'after-production-compile': '[@sentry/nextjs - After Production Compile]', + }[buildTool]; + + if (buildTool === 'after-production-compile') { + // Turbopack builds + sourcemapUploadAssets.push( + path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output ); + + if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { + filesToDeleteAfterUpload.push( + path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), + path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'), + path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'), + ); + } + } else { + if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { + sourcemapUploadAssets.push( + path.posix.join(distDirAbsPath, 'server', '**'), // Standard output location for server builds + path.posix.join(distDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js + ); + } else { + // Client builds + if (sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**')); + } else { + sourcemapUploadAssets.push( + path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'), + ); + } + + // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful + if (!sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); + } + + // Always ignore framework, polyfills, and webpack files + sourcemapUploadIgnore.push( + path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), + ); + + // File deletion for webpack client builds + if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { + filesToDeleteAfterUpload.push( + // We only care to delete client bundle source maps because they would be the ones being served. + // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: + // https://github.com/getsentry/sentry-javascript/issues/13099 + path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'), + path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'), + path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'), + ); + } + } } + // If the user has opted into using the experimental hook, we skip sourcemaps and release management in the plugin + // to avoid double sourcemap uploads. + const shouldSkipSourcemapsUpload = useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); + return { authToken: sentryBuildOptions.authToken, headers: sentryBuildOptions.headers, @@ -43,14 +106,17 @@ export function getBuildPluginOptions({ telemetry: sentryBuildOptions.telemetry, debug: sentryBuildOptions.debug, errorHandler: sentryBuildOptions.errorHandler, - reactComponentAnnotation: { - ...sentryBuildOptions.reactComponentAnnotation, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, - }, + reactComponentAnnotation: + buildTool === 'after-production-compile' + ? undefined + : { + ...sentryBuildOptions.reactComponentAnnotation, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, + }, silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { - disable: sentryBuildOptions.sourcemaps?.disable, + disable: sentryBuildOptions.sourcemaps?.disable || shouldSkipSourcemapsUpload, rewriteSources(source) { if (source.startsWith('webpack://_N_E/')) { return source.replace('webpack://_N_E/', ''); @@ -62,7 +128,7 @@ export function getBuildPluginOptions({ }, assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore, - filesToDeleteAfterUpload, + filesToDeleteAfterUpload: filesToDeleteAfterUpload.length > 0 ? filesToDeleteAfterUpload : undefined, ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, }, release: @@ -87,7 +153,7 @@ export function getBuildPluginOptions({ ...sentryBuildOptions.bundleSizeOptimizations, }, _metaOptions: { - loggerPrefixOverride: '[@sentry/nextjs]', + loggerPrefixOverride: loggerPrefix, telemetry: { metaFramework: 'nextjs', }, @@ -95,3 +161,27 @@ export function getBuildPluginOptions({ ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, }; } + +/** + * Legacy function for webpack builds. Now calls the unified getBuildPluginOptions function. + * @deprecated Use getBuildPluginOptions instead + */ +export function getWebpackPluginOptions( + buildContext: BuildContext, + sentryBuildOptions: SentryBuildOptions, + releaseName: string | undefined, +): SentryBuildPluginOptions { + const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext; + const buildTool = isServer ? (nextRuntime === 'edge' ? 'webpack-edge' : 'webpack-nodejs') : 'webpack-client'; + + const projectDir = dir.replace(/\\/g, '/'); + const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; + const distDirAbsPath = path.posix.join(projectDir, distDir); + + return getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath, + buildTool, + }); +} diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index 01979b497c72..a62f63eb79f4 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -11,12 +11,6 @@ export async function handleRunAfterProductionCompile( { releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' }, sentryBuildOptions: SentryBuildOptions, ): Promise { - // We don't want to do anything for webpack at this point because the plugin already handles this - // TODO: Actually implement this for webpack as well - if (buildTool === 'webpack') { - return; - } - if (sentryBuildOptions.debug) { // eslint-disable-next-line no-console console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); @@ -36,17 +30,17 @@ export async function handleRunAfterProductionCompile( return; } - const sentryBuildPluginManager = createSentryBuildPluginManager( - getBuildPluginOptions({ - sentryBuildOptions, - releaseName, - distDirAbsPath: distDir, - }), - { - buildTool, - loggerPrefix: '[@sentry/nextjs]', - }, - ); + const options = getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath: distDir, + buildTool: 'after-production-compile', + }); + + const sentryBuildPluginManager = createSentryBuildPluginManager(options, { + buildTool, + loggerPrefix: '[@sentry/nextjs - After Production Compile]', + }); await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8741efe81194..8a4cac5f5b07 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -7,6 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { sync as resolveSync } from 'resolve'; import type { VercelCronsConfig } from '../common/types'; +import { getBuildPluginOptions } from './getBuildPluginOptions'; import type { RouteManifest } from './manifest/types'; // Note: If you need to import a type from Webpack, do it in `types.ts` and export it from there. Otherwise, our // circular dependency check thinks this file is importing from itself. See https://github.com/pahen/madge/issues/306. @@ -22,7 +23,6 @@ import type { WebpackEntryProperty, } from './types'; import { getNextjsVersion } from './util'; -import { getWebpackPluginOptions } from './webpackPluginOptions'; // Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain // warnings 3 times, we keep track of them here. @@ -408,9 +408,21 @@ export function constructWebpackConfigFunction( } newConfig.plugins = newConfig.plugins || []; + const { config: userNextConfig, dir, nextRuntime } = buildContext; + const buildTool = isServer ? (nextRuntime === 'edge' ? 'webpack-edge' : 'webpack-nodejs') : 'webpack-client'; + const projectDir = dir.replace(/\\/g, '/'); + const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; + const distDirAbsPath = path.posix.join(projectDir, distDir); + const sentryWebpackPluginInstance = sentryWebpackPlugin( - getWebpackPluginOptions(buildContext, userSentryOptions, releaseName), + getBuildPluginOptions({ + sentryBuildOptions: userSentryOptions, + releaseName, + distDirAbsPath, + buildTool, + }), ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access sentryWebpackPluginInstance._name = 'sentry-webpack-plugin'; // For tests and debugging. Serves no other purpose. newConfig.plugins.push(sentryWebpackPluginInstance); diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts deleted file mode 100644 index f4ff4363cdb7..000000000000 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin'; -import * as path from 'path'; -import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types'; - -/** - * Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or - * client files. - */ -export function getWebpackPluginOptions( - buildContext: BuildContext, - sentryBuildOptions: SentryBuildOptions, - releaseName: string | undefined, -): SentryWebpackPluginOptions { - const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext; - - const prefixInsert = !isServer ? 'Client' : nextRuntime === 'edge' ? 'Edge' : 'Node.js'; - - // We need to convert paths to posix because Glob patterns use `\` to escape - // glob characters. This clashes with Windows path separators. - // See: https://www.npmjs.com/package/glob - const projectDir = dir.replace(/\\/g, '/'); - // `.next` is the default directory - const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; - const distDirAbsPath = path.posix.join(projectDir, distDir); - - const sourcemapUploadAssets: string[] = []; - const sourcemapUploadIgnore: string[] = []; - - if (isServer) { - sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'server', '**'), // This is normally where Next.js outputs things - path.posix.join(distDirAbsPath, 'serverless', '**'), // This was the output location for serverless Next.js - ); - } else { - if (sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**')); - } else { - sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'), - ); - } - - // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful - if (!sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); - } - - // Always ignore framework, polyfills, and webpack files - sourcemapUploadIgnore.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), - ); - } - - return { - authToken: sentryBuildOptions.authToken, - headers: sentryBuildOptions.headers, - org: sentryBuildOptions.org, - project: sentryBuildOptions.project, - telemetry: sentryBuildOptions.telemetry, - debug: sentryBuildOptions.debug, - errorHandler: sentryBuildOptions.errorHandler, - reactComponentAnnotation: { - ...sentryBuildOptions.reactComponentAnnotation, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, - }, - silent: sentryBuildOptions.silent, - url: sentryBuildOptions.sentryUrl, - sourcemaps: { - disable: sentryBuildOptions.sourcemaps?.disable, - rewriteSources(source) { - if (source.startsWith('webpack://_N_E/')) { - return source.replace('webpack://_N_E/', ''); - } else if (source.startsWith('webpack://')) { - return source.replace('webpack://', ''); - } else { - return source; - } - }, - assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, - ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore, - filesToDeleteAfterUpload: sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload - ? [ - // We only care to delete client bundle source maps because they would be the ones being served. - // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: - // https://github.com/getsentry/sentry-javascript/issues/13099 - path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'), - ] - : undefined, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, - }, - release: - releaseName !== undefined - ? { - inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. - name: releaseName, - create: sentryBuildOptions.release?.create, - finalize: sentryBuildOptions.release?.finalize, - dist: sentryBuildOptions.release?.dist, - vcsRemote: sentryBuildOptions.release?.vcsRemote, - setCommits: sentryBuildOptions.release?.setCommits, - deploy: sentryBuildOptions.release?.deploy, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, - } - : { - inject: false, - create: false, - finalize: false, - }, - bundleSizeOptimizations: { - ...sentryBuildOptions.bundleSizeOptimizations, - }, - _metaOptions: { - loggerPrefixOverride: `[@sentry/nextjs - ${prefixInsert}]`, - telemetry: { - metaFramework: 'nextjs', - }, - }, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, - }; -} From 18828a19c9b3b81f9aa69fb04d0efe92c8fc441a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 16 Sep 2025 11:08:21 +0200 Subject: [PATCH 02/21] legacy method was not exported anyway --- .../src/config/getBuildPluginOptions.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 9f1061925fd9..6bceeee5e827 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -1,6 +1,6 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin-core'; import * as path from 'path'; -import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types'; +import type { SentryBuildOptions } from './types'; /** * Get Sentry Build Plugin options for both webpack and turbopack builds. @@ -161,27 +161,3 @@ export function getBuildPluginOptions({ ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, }; } - -/** - * Legacy function for webpack builds. Now calls the unified getBuildPluginOptions function. - * @deprecated Use getBuildPluginOptions instead - */ -export function getWebpackPluginOptions( - buildContext: BuildContext, - sentryBuildOptions: SentryBuildOptions, - releaseName: string | undefined, -): SentryBuildPluginOptions { - const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext; - const buildTool = isServer ? (nextRuntime === 'edge' ? 'webpack-edge' : 'webpack-nodejs') : 'webpack-client'; - - const projectDir = dir.replace(/\\/g, '/'); - const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; - const distDirAbsPath = path.posix.join(projectDir, distDir); - - return getBuildPluginOptions({ - sentryBuildOptions, - releaseName, - distDirAbsPath, - buildTool, - }); -} From 84c2e1ac9f84b4e396da572180297c8117c70ea5 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 16 Sep 2025 13:08:33 +0200 Subject: [PATCH 03/21] update tests --- .../test/config/getBuildPluginOptions.test.ts | 236 +++++++++++++++++- .../handleRunAfterProductionCompile.test.ts | 17 +- .../webpack/constructWebpackConfig.test.ts | 23 +- .../webpack/webpackPluginOptions.test.ts | 234 ----------------- 4 files changed, 252 insertions(+), 258 deletions(-) delete mode 100644 packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 1120084ec76e..972e380f62e0 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -7,7 +7,7 @@ describe('getBuildPluginOptions', () => { const mockDistDirAbsPath = '/path/to/.next'; describe('basic functionality', () => { - it('returns correct build plugin options with minimal configuration', () => { + it('returns correct build plugin options with minimal configuration for after-production-compile', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -18,6 +18,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', }); expect(result).toMatchObject({ @@ -27,7 +28,7 @@ describe('getBuildPluginOptions', () => { sourcemaps: { assets: ['/path/to/.next/**'], ignore: [], - filesToDeleteAfterUpload: [], + filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }, release: { @@ -37,12 +38,13 @@ describe('getBuildPluginOptions', () => { finalize: undefined, }, _metaOptions: { - loggerPrefixOverride: '[@sentry/nextjs]', + loggerPrefixOverride: '[@sentry/nextjs - After Production Compile]', telemetry: { metaFramework: 'nextjs', }, }, bundleSizeOptimizations: {}, + reactComponentAnnotation: undefined, // Should be undefined for after-production-compile }); }); @@ -57,14 +59,154 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: windowsPath, + buildTool: 'after-production-compile', }); expect(result.sourcemaps?.assets).toEqual(['C:/Users/test/.next/**']); }); }); + describe('build tool specific behavior', () => { + const baseSentryOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + it('configures webpack-client build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Client]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/static/chunks/pages/**', + '/path/to/.next/static/chunks/app/**', + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures webpack-client build with widenClientFileUpload correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: { + ...baseSentryOptions, + widenClientFileUpload: true, + }, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + }); + + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/static/chunks/**']); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + }); + + it('configures webpack-nodejs build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-nodejs', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Node.js]'); + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); + expect(result.sourcemaps?.ignore).toEqual([]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures webpack-edge build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-edge', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Edge]'); + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); + expect(result.sourcemaps?.ignore).toEqual([]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures after-production-compile build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile]'); + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/**']); + expect(result.sourcemaps?.ignore).toEqual([]); + expect(result.reactComponentAnnotation).toBeUndefined(); + }); + }); + + describe('useRunAfterProductionCompileHook functionality', () => { + const baseSentryOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + it('disables sourcemaps when useRunAfterProductionCompileHook is true for webpack builds', () => { + const webpackBuildTools = ['webpack-client', 'webpack-nodejs', 'webpack-edge'] as const; + + webpackBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + useRunAfterProductionCompileHook: true, + }); + + expect(result.sourcemaps?.disable).toBe(true); + }); + }); + + it('does not disable sourcemaps when useRunAfterProductionCompileHook is true for after-production-compile', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', + useRunAfterProductionCompileHook: true, + }); + + expect(result.sourcemaps?.disable).toBe(false); + }); + + it('does not disable sourcemaps when useRunAfterProductionCompileHook is false', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + useRunAfterProductionCompileHook: false, + }); + + expect(result.sourcemaps?.disable).toBe(false); + }); + }); + describe('sourcemap configuration', () => { - it('configures file deletion when deleteSourcemapsAfterUpload is enabled', () => { + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -77,6 +219,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ @@ -86,6 +229,52 @@ describe('getBuildPluginOptions', () => { ]); }); + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', + ]); + }); + + it('does not configure file deletion for server builds even when deleteSourcemapsAfterUpload is enabled', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const serverBuildTools = ['webpack-nodejs', 'webpack-edge'] as const; + + serverBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + }); + it('does not configure file deletion when deleteSourcemapsAfterUpload is disabled', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -99,9 +288,10 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); - expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([]); + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); }); it('uses custom sourcemap assets when provided', () => { @@ -118,6 +308,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.assets).toEqual(customAssets); @@ -137,6 +328,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.ignore).toEqual(customIgnore); @@ -155,6 +347,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.disable).toBe(true); @@ -172,6 +365,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); const rewriteSources = result.sourcemaps?.rewriteSources; @@ -209,6 +403,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -230,6 +425,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: undefined, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -263,6 +459,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); // The unstable_sentryWebpackPluginOptions.release is spread at the end and may override base properties @@ -272,7 +469,7 @@ describe('getBuildPluginOptions', () => { }); describe('react component annotation', () => { - it('merges react component annotation options correctly', () => { + it('merges react component annotation options correctly for webpack builds', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -290,11 +487,31 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); // The unstable options override the base options - in this case enabled should be false expect(result.reactComponentAnnotation).toHaveProperty('enabled', false); }); + + it('sets react component annotation to undefined for after-production-compile', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + reactComponentAnnotation: { + enabled: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', + }); + + expect(result.reactComponentAnnotation).toBeUndefined(); + }); }); describe('other configuration options', () => { @@ -318,6 +535,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result).toMatchObject({ @@ -352,6 +570,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result).toMatchObject({ @@ -374,6 +593,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: undefined, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -394,13 +614,14 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile', }); expect(result.sourcemaps).toMatchObject({ disable: undefined, assets: ['/path/to/.next/**'], ignore: [], - filesToDeleteAfterUpload: [], + filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }); }); @@ -419,6 +640,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: complexPath, + buildTool: 'after-production-compile', }); expect(result.sourcemaps?.assets).toEqual([`${complexPath}/**`]); diff --git a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts index 22973cb6f15b..f32eb28ddcfc 100644 --- a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts +++ b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts @@ -79,7 +79,7 @@ describe('handleRunAfterProductionCompile', () => { }), { buildTool: 'turbopack', - loggerPrefix: '[@sentry/nextjs]', + loggerPrefix: '[@sentry/nextjs - After Production Compile]', }, ); }); @@ -108,7 +108,7 @@ describe('handleRunAfterProductionCompile', () => { }); describe('webpack builds', () => { - it('skips execution for webpack builds', async () => { + it('executes all build steps for webpack builds', async () => { await handleRunAfterProductionCompile( { releaseName: 'test-release', @@ -118,11 +118,16 @@ describe('handleRunAfterProductionCompile', () => { mockSentryBuildOptions, ); - expect(loadModule).not.toHaveBeenCalled(); - expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); + expect(mockSentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal).toHaveBeenCalledTimes(1); + expect(mockSentryBuildPluginManager.createRelease).toHaveBeenCalledTimes(1); + expect(mockSentryBuildPluginManager.injectDebugIds).toHaveBeenCalledWith(['/path/to/.next']); + expect(mockSentryBuildPluginManager.uploadSourcemaps).toHaveBeenCalledWith(['/path/to/.next'], { + prepareArtifacts: false, + }); + expect(mockSentryBuildPluginManager.deleteArtifacts).toHaveBeenCalledTimes(1); }); - it('does not log debug message for webpack builds when debug is enabled', async () => { + it('logs debug message for webpack builds when debug is enabled', async () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const debugOptions = { @@ -139,7 +144,7 @@ describe('handleRunAfterProductionCompile', () => { debugOptions, ); - expect(consoleSpy).not.toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + expect(consoleSpy).toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); consoleSpy.mockRestore(); }); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 7371d35c859a..b13d92775862 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -2,8 +2,8 @@ import '../mocks'; import * as core from '@sentry/core'; import { describe, expect, it, vi } from 'vitest'; +import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions'; import * as util from '../../../src/config/util'; -import * as getWebpackPluginOptionsModule from '../../../src/config/webpackPluginOptions'; import { CLIENT_SDK_CONFIG_FILE, clientBuildContext, @@ -55,7 +55,7 @@ describe('constructWebpackConfigFunction()', () => { }); it('automatically enables deleteSourcemapsAfterUpload for client builds when not explicitly set', async () => { - const getWebpackPluginOptionsSpy = vi.spyOn(getWebpackPluginOptionsModule, 'getWebpackPluginOptions'); + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); vi.spyOn(core, 'loadModule').mockImplementation(() => ({ sentryWebpackPlugin: () => ({ _name: 'sentry-webpack-plugin', @@ -71,19 +71,20 @@ describe('constructWebpackConfigFunction()', () => { }, }); - expect(getWebpackPluginOptionsSpy).toHaveBeenCalledWith( + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( expect.objectContaining({ - isServer: false, - }), - expect.objectContaining({ - sourcemaps: { - deleteSourcemapsAfterUpload: true, - }, + sentryBuildOptions: expect.objectContaining({ + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }), + buildTool: 'webpack-client', + distDirAbsPath: expect.any(String), + releaseName: undefined, }), - undefined, ); - getWebpackPluginOptionsSpy.mockRestore(); + getBuildPluginOptionsSpy.mockRestore(); }); it('preserves unrelated webpack config options', async () => { diff --git a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts deleted file mode 100644 index e95ab5c82bf8..000000000000 --- a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { BuildContext, NextConfigObject } from '../../../src/config/types'; -import { getWebpackPluginOptions } from '../../../src/config/webpackPluginOptions'; - -function generateBuildContext(overrides: { - dir?: string; - isServer: boolean; - nextjsConfig?: NextConfigObject; -}): BuildContext { - return { - dev: false, // The plugin is not included in dev mode - isServer: overrides.isServer, - buildId: 'test-build-id', - dir: overrides.dir ?? '/my/project/dir', - config: overrides.nextjsConfig ?? {}, - totalPages: 2, - defaultLoaders: true, - webpack: { - version: '4.0.0', - DefinePlugin: {} as any, - }, - }; -} - -describe('getWebpackPluginOptions()', () => { - it('forwards relevant options', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - authToken: 'my-auth-token', - headers: { 'my-test-header': 'test' }, - org: 'my-org', - project: 'my-project', - telemetry: false, - reactComponentAnnotation: { - enabled: true, - ignoredComponents: ['myComponent'], - }, - silent: false, - debug: true, - sentryUrl: 'my-url', - sourcemaps: { - assets: ['my-asset'], - ignore: ['my-ignore'], - }, - release: { - name: 'my-release', - create: false, - finalize: false, - dist: 'my-dist', - vcsRemote: 'my-origin', - setCommits: { - auto: true, - }, - deploy: { - env: 'my-env', - }, - }, - }, - 'my-release', - ); - - expect(generatedPluginOptions.authToken).toBe('my-auth-token'); - expect(generatedPluginOptions.debug).toBe(true); - expect(generatedPluginOptions.headers).toStrictEqual({ 'my-test-header': 'test' }); - expect(generatedPluginOptions.org).toBe('my-org'); - expect(generatedPluginOptions.project).toBe('my-project'); - expect(generatedPluginOptions.reactComponentAnnotation?.enabled).toBe(true); - expect(generatedPluginOptions.reactComponentAnnotation?.ignoredComponents).toStrictEqual(['myComponent']); - expect(generatedPluginOptions.release?.create).toBe(false); - expect(generatedPluginOptions.release?.deploy?.env).toBe('my-env'); - expect(generatedPluginOptions.release?.dist).toBe('my-dist'); - expect(generatedPluginOptions.release?.finalize).toBe(false); - expect(generatedPluginOptions.release?.name).toBe('my-release'); - expect(generatedPluginOptions.release?.setCommits?.auto).toBe(true); - expect(generatedPluginOptions.release?.vcsRemote).toBe('my-origin'); - expect(generatedPluginOptions.silent).toBe(false); - expect(generatedPluginOptions.sourcemaps?.assets).toStrictEqual(['my-asset']); - expect(generatedPluginOptions.sourcemaps?.ignore).toStrictEqual(['my-ignore']); - expect(generatedPluginOptions.telemetry).toBe(false); - expect(generatedPluginOptions.url).toBe('my-url'); - - expect(generatedPluginOptions).toMatchObject({ - authToken: 'my-auth-token', - debug: true, - headers: { - 'my-test-header': 'test', - }, - org: 'my-org', - project: 'my-project', - reactComponentAnnotation: { - enabled: true, - ignoredComponents: ['myComponent'], - }, - release: { - create: false, - deploy: { - env: 'my-env', - }, - dist: 'my-dist', - finalize: false, - inject: false, - name: 'my-release', - setCommits: { - auto: true, - }, - vcsRemote: 'my-origin', - }, - silent: false, - sourcemaps: { - assets: ['my-asset'], - ignore: ['my-ignore'], - }, - telemetry: false, - url: 'my-url', - }); - }); - - it('forwards bundleSizeOptimization options', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - bundleSizeOptimizations: { - excludeTracing: true, - excludeReplayShadowDom: false, - }, - }, - undefined, - ); - - expect(generatedPluginOptions).toMatchObject({ - bundleSizeOptimizations: { - excludeTracing: true, - excludeReplayShadowDom: false, - }, - }); - }); - - it('forwards errorHandler option', () => { - const buildContext = generateBuildContext({ isServer: false }); - const mockErrorHandler = (err: Error) => { - throw err; - }; - - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - errorHandler: mockErrorHandler, - }, - undefined, - ); - - expect(generatedPluginOptions.errorHandler).toBe(mockErrorHandler); - }); - - it('returns the right `assets` and `ignore` values during the server build', () => { - const buildContext = generateBuildContext({ isServer: true }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/server/**', '/my/project/dir/.next/serverless/**'], - ignore: [], - }); - }); - - it('returns the right `assets` and `ignore` values during the client build', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/static/chunks/pages/**', '/my/project/dir/.next/static/chunks/app/**'], - ignore: [ - '/my/project/dir/.next/static/chunks/main-*', - '/my/project/dir/.next/static/chunks/framework-*', - '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/polyfills-*', - '/my/project/dir/.next/static/chunks/webpack-*', - ], - }); - }); - - it('returns the right `assets` and `ignore` values during the client build with `widenClientFileUpload`', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/static/chunks/**'], - ignore: [ - '/my/project/dir/.next/static/chunks/framework-*', - '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/polyfills-*', - '/my/project/dir/.next/static/chunks/webpack-*', - ], - }); - }); - - it('sets `sourcemaps.disable` plugin options to true when `sourcemaps.disable` is true', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - disable: true, - }); - }); - - it('passes posix paths to the plugin', () => { - const buildContext = generateBuildContext({ - dir: 'C:\\my\\windows\\project\\dir', - nextjsConfig: { distDir: '.dist\\v1' }, - isServer: false, - }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['C:/my/windows/project/dir/.dist/v1/static/chunks/**'], - ignore: [ - 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework-*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework.*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/polyfills-*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/webpack-*', - ], - }); - }); - - it('sets options to not create a release or do any release operations when releaseName is undefined', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - - expect(generatedPluginOptions).toMatchObject({ - release: { - inject: false, - create: false, - finalize: false, - }, - }); - }); -}); From ca81ae00bd64d2d963218e430632e37f666f3ff2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 17 Sep 2025 13:14:50 +0200 Subject: [PATCH 04/21] use cli for webpack --- .../src/config/getBuildPluginOptions.ts | 8 +- packages/nextjs/src/config/webpack.ts | 23 ++++-- .../nextjs/src/config/withSentryConfig.ts | 7 +- .../test/config/getBuildPluginOptions.test.ts | 2 +- packages/nextjs/test/config/testUtils.ts | 17 ++-- .../webpack/constructWebpackConfig.test.ts | 80 +++++++++++++++++++ 6 files changed, 117 insertions(+), 20 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 6bceeee5e827..78b3d7b73524 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -81,7 +81,8 @@ export function getBuildPluginOptions({ ); // File deletion for webpack client builds - if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { + // If the user has opted into using the experimental hook, we delete the source maps in the hook instead + if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload && !useRunAfterProductionCompileHook) { filesToDeleteAfterUpload.push( // We only care to delete client bundle source maps because they would be the ones being served. // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: @@ -94,8 +95,7 @@ export function getBuildPluginOptions({ } } - // If the user has opted into using the experimental hook, we skip sourcemaps and release management in the plugin - // to avoid double sourcemap uploads. + // If the user has opted into using the experimental hook, we skip sourcemap uploads in the plugin const shouldSkipSourcemapsUpload = useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); return { @@ -116,7 +116,7 @@ export function getBuildPluginOptions({ silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { - disable: sentryBuildOptions.sourcemaps?.disable || shouldSkipSourcemapsUpload, + disable: shouldSkipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false), rewriteSources(source) { if (source.startsWith('webpack://_N_E/')) { return source.replace('webpack://_N_E/', ''); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8a4cac5f5b07..93e0dc525401 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -40,13 +40,21 @@ let showedMissingGlobalErrorWarningMsg = false; * @param userSentryOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` * @returns The function to set as the nextjs config's `webpack` value */ -export function constructWebpackConfigFunction( - userNextConfig: NextConfigObject = {}, - userSentryOptions: SentryBuildOptions = {}, - releaseName: string | undefined, - routeManifest: RouteManifest | undefined, - nextJsVersion: string | undefined, -): WebpackConfigFunction { +export function constructWebpackConfigFunction({ + userNextConfig = {}, + userSentryOptions = {}, + releaseName, + routeManifest, + nextJsVersion, + useRunAfterProductionCompileHook, +}: { + userNextConfig: NextConfigObject; + userSentryOptions: SentryBuildOptions; + releaseName: string | undefined; + routeManifest: RouteManifest | undefined; + nextJsVersion: string | undefined; + useRunAfterProductionCompileHook: boolean | undefined; +}): WebpackConfigFunction { // Will be called by nextjs and passed its default webpack configuration and context data about the build (whether // we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that // `incomingConfig` and `buildContext` are referred to as `config` and `options` in the nextjs docs. @@ -420,6 +428,7 @@ export function constructWebpackConfigFunction( releaseName, distDirAbsPath, buildTool, + useRunAfterProductionCompileHook, }), ); diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 4558e5349c5a..7800cef56250 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -368,13 +368,14 @@ function getFinalConfigObject( webpack: isTurbopack || userSentryOptions.disableSentryWebpackConfig ? incomingUserNextConfigObject.webpack // just return the original webpack config - : constructWebpackConfigFunction( - incomingUserNextConfigObject, + : constructWebpackConfigFunction({ + userNextConfig: incomingUserNextConfigObject, userSentryOptions, releaseName, routeManifest, nextJsVersion, - ), + useRunAfterProductionCompileHook: userSentryOptions._experimental?.useRunAfterProductionCompileHook, + }), ...(isTurbopackSupported && isTurbopack ? { turbopack: constructTurbopackConfig({ diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 972e380f62e0..eb66bd60d31d 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -618,7 +618,7 @@ describe('getBuildPluginOptions', () => { }); expect(result.sourcemaps).toMatchObject({ - disable: undefined, + disable: false, assets: ['/path/to/.next/**'], ignore: [], filesToDeleteAfterUpload: undefined, diff --git a/packages/nextjs/test/config/testUtils.ts b/packages/nextjs/test/config/testUtils.ts index 19e2a8f1c326..9b7191ea73be 100644 --- a/packages/nextjs/test/config/testUtils.ts +++ b/packages/nextjs/test/config/testUtils.ts @@ -56,6 +56,10 @@ export async function materializeFinalWebpackConfig(options: { incomingWebpackConfig: WebpackConfigObject; incomingWebpackBuildContext: BuildContext; sentryBuildTimeOptions?: SentryBuildOptions; + releaseName?: string; + routeManifest?: any; + nextJsVersion?: string; + useRunAfterProductionCompileHook?: boolean; }): Promise { const { exportedNextConfig, incomingWebpackConfig, incomingWebpackBuildContext } = options; @@ -66,11 +70,14 @@ export async function materializeFinalWebpackConfig(options: { : exportedNextConfig; // get the webpack config function we'd normally pass back to next - const webpackConfigFunction = constructWebpackConfigFunction( - materializedUserNextConfig, - options.sentryBuildTimeOptions, - undefined, - ); + const webpackConfigFunction = constructWebpackConfigFunction({ + userNextConfig: materializedUserNextConfig, + userSentryOptions: options.sentryBuildTimeOptions || {}, + releaseName: options.releaseName, + routeManifest: options.routeManifest, + nextJsVersion: options.nextJsVersion, + useRunAfterProductionCompileHook: options.useRunAfterProductionCompileHook ?? options.sentryBuildTimeOptions?._experimental?.useRunAfterProductionCompileHook, + }); // call it to get concrete values for comparison const finalWebpackConfigValue = webpackConfigFunction(incomingWebpackConfig, incomingWebpackBuildContext); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index b13d92775862..d46bcd917fb7 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -87,6 +87,86 @@ describe('constructWebpackConfigFunction()', () => { getBuildPluginOptionsSpy.mockRestore(); }); + it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when enabled', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + useRunAfterProductionCompileHook: true, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + + it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when disabled', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + _experimental: { + useRunAfterProductionCompileHook: false, + }, + }, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + useRunAfterProductionCompileHook: false, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + + it('passes useRunAfterProductionCompileHook as undefined when not specified', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: {}, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + useRunAfterProductionCompileHook: undefined, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + it('preserves unrelated webpack config options', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, From 547ce6adeab04bda254d479897f7dfcd94072616 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 17 Sep 2025 13:18:53 +0200 Subject: [PATCH 05/21] log turbopack sourcemaps info only in debug mode --- packages/nextjs/src/config/withSentryConfig.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 7800cef56250..494052af26f2 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -329,16 +329,21 @@ function getFinalConfigObject( if (isTurbopackSupported && isTurbopack && !userSentryOptions.sourcemaps?.disable) { // Only set if not already configured by user if (incomingUserNextConfigObject.productionBrowserSourceMaps === undefined) { - // eslint-disable-next-line no-console - console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); + if (userSentryOptions.debug) { + // eslint-disable-next-line no-console + console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); + } incomingUserNextConfigObject.productionBrowserSourceMaps = true; // Enable source map deletion if not explicitly disabled if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', - ); + if (userSentryOptions.debug) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', + ); + } + userSentryOptions.sourcemaps = { ...userSentryOptions.sourcemaps, deleteSourcemapsAfterUpload: true, From 4de658f8ba633aa925a1128b7fb6395bc5475bcb Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 17 Sep 2025 13:21:37 +0200 Subject: [PATCH 06/21] test for logs --- packages/nextjs/test/config/withSentryConfig.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 9303223c97bc..d0b30aa7eae3 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -457,7 +457,11 @@ describe('withSentryConfig', () => { const cleanConfig = { ...exportedNextConfig }; delete cleanConfig.productionBrowserSourceMaps; - materializeFinalNextConfig(cleanConfig); + const sentryOptions = { + debug: true, + }; + + materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); expect(consoleSpy).toHaveBeenCalledWith( '[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.', @@ -476,6 +480,7 @@ describe('withSentryConfig', () => { delete cleanConfig.productionBrowserSourceMaps; const sentryOptions = { + debug: true, sourcemaps: {}, // triggers automatic deletion }; From 3850b2621aeee5a747bdc4a275f31d07201ccffe Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 17 Sep 2025 14:47:47 +0200 Subject: [PATCH 07/21] format --- packages/nextjs/test/config/testUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/test/config/testUtils.ts b/packages/nextjs/test/config/testUtils.ts index 9b7191ea73be..a644525ce311 100644 --- a/packages/nextjs/test/config/testUtils.ts +++ b/packages/nextjs/test/config/testUtils.ts @@ -76,7 +76,9 @@ export async function materializeFinalWebpackConfig(options: { releaseName: options.releaseName, routeManifest: options.routeManifest, nextJsVersion: options.nextJsVersion, - useRunAfterProductionCompileHook: options.useRunAfterProductionCompileHook ?? options.sentryBuildTimeOptions?._experimental?.useRunAfterProductionCompileHook, + useRunAfterProductionCompileHook: + options.useRunAfterProductionCompileHook ?? + options.sentryBuildTimeOptions?._experimental?.useRunAfterProductionCompileHook, }); // call it to get concrete values for comparison From 6e9972ef0a8387bfe1a0af9810f0ff7737d4beb4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 18 Sep 2025 13:50:44 +0200 Subject: [PATCH 08/21] distinguish after production compile hook calls --- .../src/config/getBuildPluginOptions.ts | 68 +++-- .../config/handleRunAfterProductionCompile.ts | 2 +- .../test/config/getBuildPluginOptions.test.ts | 238 +++++++++++++++--- 3 files changed, 241 insertions(+), 67 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 78b3d7b73524..baf229496807 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -18,7 +18,12 @@ export function getBuildPluginOptions({ sentryBuildOptions: SentryBuildOptions; releaseName: string | undefined; distDirAbsPath: string; - buildTool: 'webpack-client' | 'webpack-nodejs' | 'webpack-edge' | 'after-production-compile'; + buildTool: + | 'webpack-client' + | 'webpack-nodejs' + | 'webpack-edge' + | 'after-production-compile-webpack' + | 'after-production-compile-turbopack'; useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook }): SentryBuildPluginOptions { const sourcemapUploadAssets: string[] = []; @@ -34,15 +39,26 @@ export function getBuildPluginOptions({ 'webpack-nodejs': '[@sentry/nextjs - Node.js]', 'webpack-edge': '[@sentry/nextjs - Edge]', 'webpack-client': '[@sentry/nextjs - Client]', - 'after-production-compile': '[@sentry/nextjs - After Production Compile]', + 'after-production-compile-webpack': '[@sentry/nextjs - After Production Compile (Webpack)]', + 'after-production-compile-turbopack': '[@sentry/nextjs - After Production Compile (Turbopack)]', }[buildTool]; - if (buildTool === 'after-production-compile') { - // Turbopack builds + if (buildTool.startsWith('after-production-compile')) { sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output + path.posix.join(normalizedDistDirAbsPath, 'server', '**'), // Standard output location for server builds + path.posix.join(normalizedDistDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js ); + if (buildTool === 'after-production-compile-turbopack') { + sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); + } else { + // Webpack client builds + sourcemapUploadAssets.push( + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), + ); + } + if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { filesToDeleteAfterUpload.push( path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), @@ -52,12 +68,13 @@ export function getBuildPluginOptions({ } } else { if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { + // Webpack server builds sourcemapUploadAssets.push( path.posix.join(distDirAbsPath, 'server', '**'), // Standard output location for server builds path.posix.join(distDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js ); } else { - // Client builds + // Webpack client builds if (sentryBuildOptions.widenClientFileUpload) { sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**')); } else { @@ -67,19 +84,6 @@ export function getBuildPluginOptions({ ); } - // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful - if (!sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); - } - - // Always ignore framework, polyfills, and webpack files - sourcemapUploadIgnore.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), - ); - // File deletion for webpack client builds // If the user has opted into using the experimental hook, we delete the source maps in the hook instead if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload && !useRunAfterProductionCompileHook) { @@ -95,6 +99,19 @@ export function getBuildPluginOptions({ } } + // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful + if (!sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); + } + + // Always ignore framework, polyfills, and webpack files + sourcemapUploadIgnore.push( + path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), + path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), + ); + // If the user has opted into using the experimental hook, we skip sourcemap uploads in the plugin const shouldSkipSourcemapsUpload = useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); @@ -106,13 +123,12 @@ export function getBuildPluginOptions({ telemetry: sentryBuildOptions.telemetry, debug: sentryBuildOptions.debug, errorHandler: sentryBuildOptions.errorHandler, - reactComponentAnnotation: - buildTool === 'after-production-compile' - ? undefined - : { - ...sentryBuildOptions.reactComponentAnnotation, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, - }, + reactComponentAnnotation: buildTool.startsWith('after-production-compile') + ? undefined + : { + ...sentryBuildOptions.reactComponentAnnotation, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, + }, silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index a62f63eb79f4..c8dc35918198 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -34,7 +34,7 @@ export async function handleRunAfterProductionCompile( sentryBuildOptions, releaseName, distDirAbsPath: distDir, - buildTool: 'after-production-compile', + buildTool: `after-production-compile-${buildTool}`, }); const sentryBuildPluginManager = createSentryBuildPluginManager(options, { diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index eb66bd60d31d..d9eb0c961f16 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -7,7 +7,7 @@ describe('getBuildPluginOptions', () => { const mockDistDirAbsPath = '/path/to/.next'; describe('basic functionality', () => { - it('returns correct build plugin options with minimal configuration for after-production-compile', () => { + it('returns correct build plugin options with minimal configuration for after-production-compile-webpack', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -18,7 +18,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-webpack', }); expect(result).toMatchObject({ @@ -26,8 +26,19 @@ describe('getBuildPluginOptions', () => { org: 'test-org', project: 'test-project', sourcemaps: { - assets: ['/path/to/.next/**'], - ignore: [], + assets: [ + '/path/to/.next/server/**', + '/path/to/.next/serverless/**', + '/path/to/.next/static/chunks/pages/**', + '/path/to/.next/static/chunks/app/**', + ], + ignore: [ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ], filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }, @@ -38,7 +49,7 @@ describe('getBuildPluginOptions', () => { finalize: undefined, }, _metaOptions: { - loggerPrefixOverride: '[@sentry/nextjs - After Production Compile]', + loggerPrefixOverride: '[@sentry/nextjs - After Production Compile (Webpack)]', telemetry: { metaFramework: 'nextjs', }, @@ -48,7 +59,7 @@ describe('getBuildPluginOptions', () => { }); }); - it('normalizes Windows paths to posix for glob patterns', () => { + it('normalizes Windows paths to posix for glob patterns in after-production-compile builds', () => { const windowsPath = 'C:\\Users\\test\\.next'; const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -59,10 +70,36 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: windowsPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-webpack', }); - expect(result.sourcemaps?.assets).toEqual(['C:/Users/test/.next/**']); + expect(result.sourcemaps?.assets).toEqual([ + 'C:/Users/test/.next/server/**', + 'C:/Users/test/.next/serverless/**', + 'C:/Users/test/.next/static/chunks/pages/**', + 'C:/Users/test/.next/static/chunks/app/**', + ]); + }); + + it('does not normalize paths for webpack builds', () => { + const windowsPath = 'C:\\Users\\test\\.next'; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: windowsPath, + buildTool: 'webpack-client', + }); + + // Webpack builds should use the original path without normalization + expect(result.sourcemaps?.assets).toEqual([ + 'C:\\Users\\test\\.next/static/chunks/pages/**', + 'C:\\Users\\test\\.next/static/chunks/app/**', + ]); }); }); @@ -125,7 +162,13 @@ describe('getBuildPluginOptions', () => { expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Node.js]'); expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); - expect(result.sourcemaps?.ignore).toEqual([]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); expect(result.reactComponentAnnotation).toBeDefined(); }); @@ -139,21 +182,62 @@ describe('getBuildPluginOptions', () => { expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Edge]'); expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); - expect(result.sourcemaps?.ignore).toEqual([]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); expect(result.reactComponentAnnotation).toBeDefined(); }); - it('configures after-production-compile build correctly', () => { + it('configures after-production-compile-webpack build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Webpack)]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/server/**', + '/path/to/.next/serverless/**', + '/path/to/.next/static/chunks/pages/**', + '/path/to/.next/static/chunks/app/**', + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeUndefined(); + }); + + it('configures after-production-compile-turbopack build correctly', () => { const result = getBuildPluginOptions({ sentryBuildOptions: baseSentryOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-turbopack', }); - expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile]'); - expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/**']); - expect(result.sourcemaps?.ignore).toEqual([]); + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Turbopack)]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/server/**', + '/path/to/.next/serverless/**', + '/path/to/.next/static/chunks/**', // Turbopack uses broader pattern + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); expect(result.reactComponentAnnotation).toBeUndefined(); }); }); @@ -180,16 +264,23 @@ describe('getBuildPluginOptions', () => { }); }); - it('does not disable sourcemaps when useRunAfterProductionCompileHook is true for after-production-compile', () => { - const result = getBuildPluginOptions({ - sentryBuildOptions: baseSentryOptions, - releaseName: mockReleaseName, - distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', - useRunAfterProductionCompileHook: true, - }); + it('does not disable sourcemaps when useRunAfterProductionCompileHook is true for after-production-compile builds', () => { + const afterProductionCompileBuildTools = [ + 'after-production-compile-webpack', + 'after-production-compile-turbopack', + ] as const; - expect(result.sourcemaps?.disable).toBe(false); + afterProductionCompileBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + useRunAfterProductionCompileHook: true, + }); + + expect(result.sourcemaps?.disable).toBe(false); + }); }); it('does not disable sourcemaps when useRunAfterProductionCompileHook is false', () => { @@ -206,7 +297,30 @@ describe('getBuildPluginOptions', () => { }); describe('sourcemap configuration', () => { - it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile', () => { + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile-webpack', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '/path/to/.next/**/*.js.map', + '/path/to/.next/**/*.mjs.map', + '/path/to/.next/**/*.cjs.map', + ]); + }); + + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile-turbopack', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -219,7 +333,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-turbopack', }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ @@ -229,7 +343,7 @@ describe('getBuildPluginOptions', () => { ]); }); - it('configures file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client', () => { + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client without useRunAfterProductionCompileHook', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -243,6 +357,7 @@ describe('getBuildPluginOptions', () => { releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, buildTool: 'webpack-client', + useRunAfterProductionCompileHook: false, }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ @@ -252,6 +367,27 @@ describe('getBuildPluginOptions', () => { ]); }); + it('does not configure file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client with useRunAfterProductionCompileHook', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + useRunAfterProductionCompileHook: true, + }); + + // File deletion should be undefined when using the hook + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + it('does not configure file deletion for server builds even when deleteSourcemapsAfterUpload is enabled', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -494,7 +630,7 @@ describe('getBuildPluginOptions', () => { expect(result.reactComponentAnnotation).toHaveProperty('enabled', false); }); - it('sets react component annotation to undefined for after-production-compile', () => { + it('sets react component annotation to undefined for after-production-compile builds', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -503,14 +639,21 @@ describe('getBuildPluginOptions', () => { }, }; - const result = getBuildPluginOptions({ - sentryBuildOptions, - releaseName: mockReleaseName, - distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', - }); + const afterProductionCompileBuildTools = [ + 'after-production-compile-webpack', + 'after-production-compile-turbopack', + ] as const; - expect(result.reactComponentAnnotation).toBeUndefined(); + afterProductionCompileBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + }); + + expect(result.reactComponentAnnotation).toBeUndefined(); + }); }); }); @@ -614,13 +757,24 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-webpack', }); expect(result.sourcemaps).toMatchObject({ disable: false, - assets: ['/path/to/.next/**'], - ignore: [], + assets: [ + '/path/to/.next/server/**', + '/path/to/.next/serverless/**', + '/path/to/.next/static/chunks/pages/**', + '/path/to/.next/static/chunks/app/**', + ], + ignore: [ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ], filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }); @@ -640,10 +794,14 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: complexPath, - buildTool: 'after-production-compile', + buildTool: 'after-production-compile-turbopack', }); - expect(result.sourcemaps?.assets).toEqual([`${complexPath}/**`]); + expect(result.sourcemaps?.assets).toEqual([ + `${complexPath}/server/**`, + `${complexPath}/serverless/**`, + `${complexPath}/static/chunks/**`, + ]); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ `${complexPath}/**/*.js.map`, `${complexPath}/**/*.mjs.map`, From 85c2df43a482a3228dd0b2df1447f2ee14435dd5 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 18 Sep 2025 15:56:11 +0200 Subject: [PATCH 09/21] normalize paths for webpack --- .../src/config/getBuildPluginOptions.ts | 26 +++++++++---------- .../test/config/getBuildPluginOptions.test.ts | 7 +++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index baf229496807..ff6c11da1187 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -70,17 +70,17 @@ export function getBuildPluginOptions({ if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { // Webpack server builds sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'server', '**'), // Standard output location for server builds - path.posix.join(distDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js + path.posix.join(normalizedDistDirAbsPath, 'server', '**'), // Standard output location for server builds + path.posix.join(normalizedDistDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js ); } else { // Webpack client builds if (sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**')); + sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); } else { sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), ); } @@ -91,9 +91,9 @@ export function getBuildPluginOptions({ // We only care to delete client bundle source maps because they would be the ones being served. // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: // https://github.com/getsentry/sentry-javascript/issues/13099 - path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'), + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.js.map'), + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.mjs.map'), + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.cjs.map'), ); } } @@ -101,15 +101,15 @@ export function getBuildPluginOptions({ // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful if (!sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); + sourcemapUploadIgnore.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'main-*')); } // Always ignore framework, polyfills, and webpack files sourcemapUploadIgnore.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'framework-*'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'framework.*'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'polyfills-*'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'webpack-*'), ); // If the user has opted into using the experimental hook, we skip sourcemap uploads in the plugin diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index d9eb0c961f16..35f3b4b915f8 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -81,7 +81,7 @@ describe('getBuildPluginOptions', () => { ]); }); - it('does not normalize paths for webpack builds', () => { + it('normalizes Windows paths to posix for webpack builds', () => { const windowsPath = 'C:\\Users\\test\\.next'; const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -95,10 +95,9 @@ describe('getBuildPluginOptions', () => { buildTool: 'webpack-client', }); - // Webpack builds should use the original path without normalization expect(result.sourcemaps?.assets).toEqual([ - 'C:\\Users\\test\\.next/static/chunks/pages/**', - 'C:\\Users\\test\\.next/static/chunks/app/**', + 'C:/Users/test/.next/static/chunks/pages/**', + 'C:/Users/test/.next/static/chunks/app/**', ]); }); }); From dc95cc02673ceee20fe61bc2f4a8a15e40d4134d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 18 Sep 2025 17:28:41 +0200 Subject: [PATCH 10/21] don't delete server source maps --- .../src/config/getBuildPluginOptions.ts | 21 ++++++++++++------- .../test/config/getBuildPluginOptions.test.ts | 18 ++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index ff6c11da1187..09c16698b0b8 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -53,17 +53,24 @@ export function getBuildPluginOptions({ sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); } else { // Webpack client builds - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), - ); + if (sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); + } else { + sourcemapUploadAssets.push( + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), + path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), + ); + } } if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { filesToDeleteAfterUpload.push( - path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'), + // We only care to delete client bundle source maps because they would be the ones being served. + // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: + // https://github.com/getsentry/sentry-javascript/issues/13099 + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.js.map'), + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.mjs.map'), + path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.cjs.map'), ); } } else { diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 35f3b4b915f8..8eef13653385 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -313,9 +313,9 @@ describe('getBuildPluginOptions', () => { }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ - '/path/to/.next/**/*.js.map', - '/path/to/.next/**/*.mjs.map', - '/path/to/.next/**/*.cjs.map', + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', ]); }); @@ -336,9 +336,9 @@ describe('getBuildPluginOptions', () => { }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ - '/path/to/.next/**/*.js.map', - '/path/to/.next/**/*.mjs.map', - '/path/to/.next/**/*.cjs.map', + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', ]); }); @@ -802,9 +802,9 @@ describe('getBuildPluginOptions', () => { `${complexPath}/static/chunks/**`, ]); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ - `${complexPath}/**/*.js.map`, - `${complexPath}/**/*.mjs.map`, - `${complexPath}/**/*.cjs.map`, + `${complexPath}/static/**/*.js.map`, + `${complexPath}/static/**/*.mjs.map`, + `${complexPath}/static/**/*.cjs.map`, ]); }); }); From e25dfe536f2de67dbb508029e52c356cdbd5bdc7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 18 Sep 2025 17:50:01 +0200 Subject: [PATCH 11/21] refactor --- .../src/config/getBuildPluginOptions.ts | 321 +++++++++++------- 1 file changed, 200 insertions(+), 121 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 09c16698b0b8..9fe7c13b0991 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -2,125 +2,229 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin import * as path from 'path'; import type { SentryBuildOptions } from './types'; +const LOGGER_PREFIXES = { + 'webpack-nodejs': '[@sentry/nextjs - Node.js]', + 'webpack-edge': '[@sentry/nextjs - Edge]', + 'webpack-client': '[@sentry/nextjs - Client]', + 'after-production-compile-webpack': '[@sentry/nextjs - After Production Compile (Webpack)]', + 'after-production-compile-turbopack': '[@sentry/nextjs - After Production Compile (Turbopack)]', +} as const; + +// File patterns for source map operations +const FILE_PATTERNS = { + SERVER: 'server/**', + SERVERLESS: 'serverless/**', + STATIC_CHUNKS: 'static/chunks/**', + STATIC_CHUNKS_PAGES: 'static/chunks/pages/**', + STATIC_CHUNKS_APP: 'static/chunks/app/**', + MAIN_CHUNKS: 'static/chunks/main-*', + FRAMEWORK_CHUNKS: 'static/chunks/framework-*', + FRAMEWORK_CHUNKS_DOT: 'static/chunks/framework.*', + POLYFILLS_CHUNKS: 'static/chunks/polyfills-*', + WEBPACK_CHUNKS: 'static/chunks/webpack-*', +} as const; + +// Source map file extensions to delete +const SOURCEMAP_EXTENSIONS = ['*.js.map', '*.mjs.map', '*.cjs.map'] as const; + +type BuildTool = keyof typeof LOGGER_PREFIXES; + /** - * Get Sentry Build Plugin options for both webpack and turbopack builds. - * These options can be used in two ways: - * 1. The build can be done in a single operation after the production build completes - * 2. The build can be done in multiple operations, one for each webpack build + * Normalizes Windows paths to POSIX format for glob patterns */ -export function getBuildPluginOptions({ - sentryBuildOptions, - releaseName, - distDirAbsPath, - buildTool, - useRunAfterProductionCompileHook, -}: { - sentryBuildOptions: SentryBuildOptions; - releaseName: string | undefined; - distDirAbsPath: string; - buildTool: - | 'webpack-client' - | 'webpack-nodejs' - | 'webpack-edge' - | 'after-production-compile-webpack' - | 'after-production-compile-turbopack'; - useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook -}): SentryBuildPluginOptions { - const sourcemapUploadAssets: string[] = []; - const sourcemapUploadIgnore: string[] = []; - const filesToDeleteAfterUpload: string[] = []; - - // We need to convert paths to posix because Glob patterns use `\` to escape - // glob characters. This clashes with Windows path separators. - // See: https://www.npmjs.com/package/glob - const normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/'); +function normalizePathForGlob(distPath: string): string { + return distPath.replace(/\\/g, '/'); +} - const loggerPrefix = { - 'webpack-nodejs': '[@sentry/nextjs - Node.js]', - 'webpack-edge': '[@sentry/nextjs - Edge]', - 'webpack-client': '[@sentry/nextjs - Client]', - 'after-production-compile-webpack': '[@sentry/nextjs - After Production Compile (Webpack)]', - 'after-production-compile-turbopack': '[@sentry/nextjs - After Production Compile (Turbopack)]', - }[buildTool]; +/** + * Creates file patterns for source map uploads based on build tool and options + */ +function createSourcemapUploadAssets( + normalizedDistPath: string, + buildTool: BuildTool, + widenClientFileUpload: boolean = false, +): string[] { + const assets: string[] = []; if (buildTool.startsWith('after-production-compile')) { - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, 'server', '**'), // Standard output location for server builds - path.posix.join(normalizedDistDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js + assets.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVER), + path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), ); if (buildTool === 'after-production-compile-turbopack') { - sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); + assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); } else { - // Webpack client builds - if (sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); + // Webpack client builds in after-production-compile mode + if (widenClientFileUpload) { + assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); } else { - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), + assets.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_PAGES), + path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_APP), ); } } - - if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { - filesToDeleteAfterUpload.push( - // We only care to delete client bundle source maps because they would be the ones being served. - // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: - // https://github.com/getsentry/sentry-javascript/issues/13099 - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.js.map'), - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.mjs.map'), - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.cjs.map'), - ); - } } else { if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { - // Webpack server builds - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, 'server', '**'), // Standard output location for server builds - path.posix.join(normalizedDistDirAbsPath, 'serverless', '**'), // Legacy output location for serverless Next.js + // Server builds + assets.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVER), + path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), ); } else { - // Webpack client builds - if (sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadAssets.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', '**')); + // Client builds + if (widenClientFileUpload) { + assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); } else { - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'app', '**'), - ); - } - - // File deletion for webpack client builds - // If the user has opted into using the experimental hook, we delete the source maps in the hook instead - if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload && !useRunAfterProductionCompileHook) { - filesToDeleteAfterUpload.push( - // We only care to delete client bundle source maps because they would be the ones being served. - // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: - // https://github.com/getsentry/sentry-javascript/issues/13099 - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.js.map'), - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.mjs.map'), - path.posix.join(normalizedDistDirAbsPath, 'static', '**', '*.cjs.map'), + assets.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_PAGES), + path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_APP), ); } } } - // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful - if (!sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadIgnore.push(path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'main-*')); + return assets; +} + +/** + * Creates ignore patterns for source map uploads + */ +function createSourcemapUploadIgnore(normalizedDistPath: string, widenClientFileUpload: boolean = false): string[] { + const ignore: string[] = []; + + // We only add main-* files if the user has not opted into it + if (!widenClientFileUpload) { + ignore.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.MAIN_CHUNKS)); + } + + // Always ignore these patterns + ignore.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS), + path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS_DOT), + path.posix.join(normalizedDistPath, FILE_PATTERNS.POLYFILLS_CHUNKS), + path.posix.join(normalizedDistPath, FILE_PATTERNS.WEBPACK_CHUNKS), + ); + + return ignore; +} + +/** + * Creates file patterns for deletion after source map upload + */ +function createFilesToDeleteAfterUpload( + normalizedDistPath: string, + buildTool: BuildTool, + deleteSourcemapsAfterUpload: boolean, + useRunAfterProductionCompileHook: boolean = false, +): string[] | undefined { + if (!deleteSourcemapsAfterUpload) { + return undefined; + } + + // We don't want to delete source maps for server builds as this led to errors on Vercel in the past + // See: https://github.com/getsentry/sentry-javascript/issues/13099 + if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { + return undefined; } - // Always ignore framework, polyfills, and webpack files - sourcemapUploadIgnore.push( - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'framework-*'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'polyfills-*'), - path.posix.join(normalizedDistDirAbsPath, 'static', 'chunks', 'webpack-*'), + // Skip deletion for webpack client builds when using the experimental hook + if (buildTool === 'webpack-client' && useRunAfterProductionCompileHook) { + return undefined; + } + + return SOURCEMAP_EXTENSIONS.map(ext => path.posix.join(normalizedDistPath, 'static', '**', ext)); +} + +/** + * Determines if sourcemap uploads should be skipped + */ +function shouldSkipSourcemapUpload(buildTool: BuildTool, useRunAfterProductionCompileHook: boolean = false): boolean { + return useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); +} + +/** + * Source rewriting function for webpack sources + */ +function rewriteWebpackSources(source: string): string { + if (source.startsWith('webpack://_N_E/')) { + return source.replace('webpack://_N_E/', ''); + } else if (source.startsWith('webpack://')) { + return source.replace('webpack://', ''); + } else { + return source; + } +} + +/** + * Creates release configuration + */ +function createReleaseConfig( + releaseName: string | undefined, + sentryBuildOptions: SentryBuildOptions, +): SentryBuildPluginOptions['release'] { + if (releaseName !== undefined) { + return { + inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. + name: releaseName, + create: sentryBuildOptions.release?.create, + finalize: sentryBuildOptions.release?.finalize, + dist: sentryBuildOptions.release?.dist, + vcsRemote: sentryBuildOptions.release?.vcsRemote, + setCommits: sentryBuildOptions.release?.setCommits, + deploy: sentryBuildOptions.release?.deploy, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, + }; + } + + return { + inject: false, + create: false, + finalize: false, + }; +} + +/** + * Get Sentry Build Plugin options for both webpack and turbopack builds. + * These options can be used in two ways: + * 1. The options can be built in a single operation after the production build completes + * 2. The options can be built in multiple operations, one for each webpack build + */ +export function getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath, + buildTool, + useRunAfterProductionCompileHook, +}: { + sentryBuildOptions: SentryBuildOptions; + releaseName: string | undefined; + distDirAbsPath: string; + buildTool: BuildTool; + useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook +}): SentryBuildPluginOptions { + // We need to convert paths to posix because Glob patterns use `\` to escape + // glob characters. This clashes with Windows path separators. + // See: https://www.npmjs.com/package/glob + const normalizedDistDirAbsPath = normalizePathForGlob(distDirAbsPath); + + const loggerPrefix = LOGGER_PREFIXES[buildTool]; + const widenClientFileUpload = sentryBuildOptions.widenClientFileUpload ?? false; + const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false; + + const sourcemapUploadAssets = createSourcemapUploadAssets(normalizedDistDirAbsPath, buildTool, widenClientFileUpload); + + const sourcemapUploadIgnore = createSourcemapUploadIgnore(normalizedDistDirAbsPath, widenClientFileUpload); + + const filesToDeleteAfterUpload = createFilesToDeleteAfterUpload( + normalizedDistDirAbsPath, + buildTool, + deleteSourcemapsAfterUpload, + useRunAfterProductionCompileHook, ); - // If the user has opted into using the experimental hook, we skip sourcemap uploads in the plugin - const shouldSkipSourcemapsUpload = useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); + const skipSourcemapsUpload = shouldSkipSourcemapUpload(buildTool, useRunAfterProductionCompileHook); return { authToken: sentryBuildOptions.authToken, @@ -139,39 +243,14 @@ export function getBuildPluginOptions({ silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { - disable: shouldSkipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false), - rewriteSources(source) { - if (source.startsWith('webpack://_N_E/')) { - return source.replace('webpack://_N_E/', ''); - } else if (source.startsWith('webpack://')) { - return source.replace('webpack://', ''); - } else { - return source; - } - }, + disable: skipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false), + rewriteSources: rewriteWebpackSources, assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore, - filesToDeleteAfterUpload: filesToDeleteAfterUpload.length > 0 ? filesToDeleteAfterUpload : undefined, + filesToDeleteAfterUpload, ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, }, - release: - releaseName !== undefined - ? { - inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. - name: releaseName, - create: sentryBuildOptions.release?.create, - finalize: sentryBuildOptions.release?.finalize, - dist: sentryBuildOptions.release?.dist, - vcsRemote: sentryBuildOptions.release?.vcsRemote, - setCommits: sentryBuildOptions.release?.setCommits, - deploy: sentryBuildOptions.release?.deploy, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, - } - : { - inject: false, - create: false, - finalize: false, - }, + release: createReleaseConfig(releaseName, sentryBuildOptions), bundleSizeOptimizations: { ...sentryBuildOptions.bundleSizeOptimizations, }, From b4aa774b93b4adb20eda117faab4a0807405343d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 00:34:47 +0200 Subject: [PATCH 12/21] are we done? --- .../src/config/getBuildPluginOptions.ts | 61 ++++++++++++++----- .../test/config/getBuildPluginOptions.test.ts | 39 ++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 9fe7c13b0991..106925fe1b1d 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -12,11 +12,23 @@ const LOGGER_PREFIXES = { // File patterns for source map operations const FILE_PATTERNS = { - SERVER: 'server/**', + SERVER: { + GLOB: 'server/**', + PATH: 'server', + }, SERVERLESS: 'serverless/**', - STATIC_CHUNKS: 'static/chunks/**', - STATIC_CHUNKS_PAGES: 'static/chunks/pages/**', - STATIC_CHUNKS_APP: 'static/chunks/app/**', + STATIC_CHUNKS: { + GLOB: 'static/chunks/**', + PATH: 'static/chunks', + }, + STATIC_CHUNKS_PAGES: { + GLOB: 'static/chunks/pages/**', + PATH: 'static/chunks/pages', + }, + STATIC_CHUNKS_APP: { + GLOB: 'static/chunks/app/**', + PATH: 'static/chunks/app', + }, MAIN_CHUNKS: 'static/chunks/main-*', FRAMEWORK_CHUNKS: 'static/chunks/framework-*', FRAMEWORK_CHUNKS_DOT: 'static/chunks/framework.*', @@ -36,6 +48,26 @@ function normalizePathForGlob(distPath: string): string { return distPath.replace(/\\/g, '/'); } +/** + * These functions are used to get the correct pattern for the sourcemap upload based on the build tool and the usage context + * -> Direct CLI invocation handles file paths better than glob patterns + */ +function getServerPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.SERVER.PATH : FILE_PATTERNS.SERVER.GLOB; +} + +function getStaticChunksPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS.PATH : FILE_PATTERNS.STATIC_CHUNKS.GLOB; +} + +function getStaticChunksPagesPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_PAGES.PATH : FILE_PATTERNS.STATIC_CHUNKS_PAGES.GLOB; +} + +function getStaticChunksAppPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_APP.PATH : FILE_PATTERNS.STATIC_CHUNKS_APP.GLOB; +} + /** * Creates file patterns for source map uploads based on build tool and options */ @@ -47,21 +79,18 @@ function createSourcemapUploadAssets( const assets: string[] = []; if (buildTool.startsWith('after-production-compile')) { - assets.push( - path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVER), - path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), - ); + assets.push(path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: true }))); if (buildTool === 'after-production-compile-turbopack') { - assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }))); } else { // Webpack client builds in after-production-compile mode if (widenClientFileUpload) { - assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }))); } else { assets.push( - path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_PAGES), - path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_APP), + path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: true })), + path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: true })), ); } } @@ -69,17 +98,17 @@ function createSourcemapUploadAssets( if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { // Server builds assets.push( - path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVER), + path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: false })), path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), ); } else { // Client builds if (widenClientFileUpload) { - assets.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS)); + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: false }))); } else { assets.push( - path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_PAGES), - path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_CHUNKS_APP), + path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: false })), + path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: false })), ); } } diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 8eef13653385..0281624584d0 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -26,12 +26,7 @@ describe('getBuildPluginOptions', () => { org: 'test-org', project: 'test-project', sourcemaps: { - assets: [ - '/path/to/.next/server/**', - '/path/to/.next/serverless/**', - '/path/to/.next/static/chunks/pages/**', - '/path/to/.next/static/chunks/app/**', - ], + assets: ['/path/to/.next/server', '/path/to/.next/static/chunks/pages', '/path/to/.next/static/chunks/app'], ignore: [ '/path/to/.next/static/chunks/main-*', '/path/to/.next/static/chunks/framework-*', @@ -74,10 +69,9 @@ describe('getBuildPluginOptions', () => { }); expect(result.sourcemaps?.assets).toEqual([ - 'C:/Users/test/.next/server/**', - 'C:/Users/test/.next/serverless/**', - 'C:/Users/test/.next/static/chunks/pages/**', - 'C:/Users/test/.next/static/chunks/app/**', + 'C:/Users/test/.next/server', + 'C:/Users/test/.next/static/chunks/pages', + 'C:/Users/test/.next/static/chunks/app', ]); }); @@ -201,10 +195,9 @@ describe('getBuildPluginOptions', () => { expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Webpack)]'); expect(result.sourcemaps?.assets).toEqual([ - '/path/to/.next/server/**', - '/path/to/.next/serverless/**', - '/path/to/.next/static/chunks/pages/**', - '/path/to/.next/static/chunks/app/**', + '/path/to/.next/server', + '/path/to/.next/static/chunks/pages', + '/path/to/.next/static/chunks/app', ]); expect(result.sourcemaps?.ignore).toEqual([ '/path/to/.next/static/chunks/main-*', @@ -226,9 +219,8 @@ describe('getBuildPluginOptions', () => { expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Turbopack)]'); expect(result.sourcemaps?.assets).toEqual([ - '/path/to/.next/server/**', - '/path/to/.next/serverless/**', - '/path/to/.next/static/chunks/**', // Turbopack uses broader pattern + '/path/to/.next/server', + '/path/to/.next/static/chunks', // Turbopack uses broader pattern ]); expect(result.sourcemaps?.ignore).toEqual([ '/path/to/.next/static/chunks/main-*', @@ -761,12 +753,7 @@ describe('getBuildPluginOptions', () => { expect(result.sourcemaps).toMatchObject({ disable: false, - assets: [ - '/path/to/.next/server/**', - '/path/to/.next/serverless/**', - '/path/to/.next/static/chunks/pages/**', - '/path/to/.next/static/chunks/app/**', - ], + assets: ['/path/to/.next/server', '/path/to/.next/static/chunks/pages', '/path/to/.next/static/chunks/app'], ignore: [ '/path/to/.next/static/chunks/main-*', '/path/to/.next/static/chunks/framework-*', @@ -796,11 +783,7 @@ describe('getBuildPluginOptions', () => { buildTool: 'after-production-compile-turbopack', }); - expect(result.sourcemaps?.assets).toEqual([ - `${complexPath}/server/**`, - `${complexPath}/serverless/**`, - `${complexPath}/static/chunks/**`, - ]); + expect(result.sourcemaps?.assets).toEqual([`${complexPath}/server`, `${complexPath}/static/chunks`]); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ `${complexPath}/static/**/*.js.map`, `${complexPath}/static/**/*.mjs.map`, From cee39bd99bb5485c2ca2d10485a7e4cc5f88f47d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 00:39:09 +0200 Subject: [PATCH 13/21] . --- packages/nextjs/src/config/getBuildPluginOptions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 106925fe1b1d..d5e34174bf7a 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -11,6 +11,9 @@ const LOGGER_PREFIXES = { } as const; // File patterns for source map operations +// We use both glob patterns and directory paths for the sourcemap upload and deletion +// -> Direct CLI invocation handles file paths better than glob patterns +// -> Webpack/Bundler needs glob patterns as this is the format that is used by the plugin const FILE_PATTERNS = { SERVER: { GLOB: 'server/**', From 25de0de99e79dbcae4809dedd8882ef046580358 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 09:53:35 +0200 Subject: [PATCH 14/21] comment + rename func --- packages/nextjs/src/config/getBuildPluginOptions.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index d5e34174bf7a..2ccbc073f204 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -74,7 +74,7 @@ function getStaticChunksAppPattern({ useDirectoryPath = false }: { useDirectoryP /** * Creates file patterns for source map uploads based on build tool and options */ -function createSourcemapUploadAssets( +function createSourcemapUploadAssetPatterns( normalizedDistPath: string, buildTool: BuildTool, widenClientFileUpload: boolean = false, @@ -85,6 +85,8 @@ function createSourcemapUploadAssets( assets.push(path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: true }))); if (buildTool === 'after-production-compile-turbopack') { + // In turbopack we always want to upload the full static chunks directory + // as the build output is not split into pages|app chunks assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }))); } else { // Webpack client builds in after-production-compile mode @@ -245,7 +247,11 @@ export function getBuildPluginOptions({ const widenClientFileUpload = sentryBuildOptions.widenClientFileUpload ?? false; const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false; - const sourcemapUploadAssets = createSourcemapUploadAssets(normalizedDistDirAbsPath, buildTool, widenClientFileUpload); + const sourcemapUploadAssets = createSourcemapUploadAssetPatterns( + normalizedDistDirAbsPath, + buildTool, + widenClientFileUpload, + ); const sourcemapUploadIgnore = createSourcemapUploadIgnore(normalizedDistDirAbsPath, widenClientFileUpload); From 8199f16753d17c3855843c9d88e7dbeca4e43cc6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 09:55:23 +0200 Subject: [PATCH 15/21] more renaming --- packages/nextjs/src/config/getBuildPluginOptions.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 2ccbc073f204..65c3a9c161b8 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -125,7 +125,10 @@ function createSourcemapUploadAssetPatterns( /** * Creates ignore patterns for source map uploads */ -function createSourcemapUploadIgnore(normalizedDistPath: string, widenClientFileUpload: boolean = false): string[] { +function createSourcemapUploadIgnorePattern( + normalizedDistPath: string, + widenClientFileUpload: boolean = false, +): string[] { const ignore: string[] = []; // We only add main-* files if the user has not opted into it @@ -147,7 +150,7 @@ function createSourcemapUploadIgnore(normalizedDistPath: string, widenClientFile /** * Creates file patterns for deletion after source map upload */ -function createFilesToDeleteAfterUpload( +function createFilesToDeleteAfterUploadPattern( normalizedDistPath: string, buildTool: BuildTool, deleteSourcemapsAfterUpload: boolean, @@ -253,9 +256,9 @@ export function getBuildPluginOptions({ widenClientFileUpload, ); - const sourcemapUploadIgnore = createSourcemapUploadIgnore(normalizedDistDirAbsPath, widenClientFileUpload); + const sourcemapUploadIgnore = createSourcemapUploadIgnorePattern(normalizedDistDirAbsPath, widenClientFileUpload); - const filesToDeleteAfterUpload = createFilesToDeleteAfterUpload( + const filesToDeleteAfterUpload = createFilesToDeleteAfterUploadPattern( normalizedDistDirAbsPath, buildTool, deleteSourcemapsAfterUpload, From cfdf388028bda31a5bf860a99fb01a99c41a8922 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 09:56:53 +0200 Subject: [PATCH 16/21] add client check --- packages/nextjs/src/config/getBuildPluginOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 65c3a9c161b8..3f2f08baa7af 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -106,7 +106,7 @@ function createSourcemapUploadAssetPatterns( path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: false })), path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), ); - } else { + } else if (buildTool === 'webpack-client') { // Client builds if (widenClientFileUpload) { assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: false }))); From c17426c8d52b389b64a42a436693fc48cbf8a40f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 10:03:29 +0200 Subject: [PATCH 17/21] use regex for rewrite sources --- packages/nextjs/src/config/getBuildPluginOptions.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 3f2f08baa7af..c676fadd7d0c 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -185,13 +185,7 @@ function shouldSkipSourcemapUpload(buildTool: BuildTool, useRunAfterProductionCo * Source rewriting function for webpack sources */ function rewriteWebpackSources(source: string): string { - if (source.startsWith('webpack://_N_E/')) { - return source.replace('webpack://_N_E/', ''); - } else if (source.startsWith('webpack://')) { - return source.replace('webpack://', ''); - } else { - return source; - } + return source.replace(/^webpack:\/\/(?:_N_E\/)?/, ''); } /** From 6fc29fb8b33a1ed1197a301ae3cfdfd583095e8c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 10:53:40 +0200 Subject: [PATCH 18/21] Update packages/nextjs/src/config/getBuildPluginOptions.ts Co-authored-by: Lukas Stracke --- packages/nextjs/src/config/getBuildPluginOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index c676fadd7d0c..8162d2291919 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -48,7 +48,7 @@ type BuildTool = keyof typeof LOGGER_PREFIXES; * Normalizes Windows paths to POSIX format for glob patterns */ function normalizePathForGlob(distPath: string): string { - return distPath.replace(/\\/g, '/'); + return distPath.replaceAll(path.sep, path.posix.sep) } /** From 131b2a20e297e8b4f980eb2109b6163bc697a89e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 10:58:40 +0200 Subject: [PATCH 19/21] revert path.sep --- packages/nextjs/src/config/getBuildPluginOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 8162d2291919..873a28b24128 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -48,7 +48,7 @@ type BuildTool = keyof typeof LOGGER_PREFIXES; * Normalizes Windows paths to POSIX format for glob patterns */ function normalizePathForGlob(distPath: string): string { - return distPath.replaceAll(path.sep, path.posix.sep) + return distPath.replaceAll('\\', '/'); } /** From beee097dc3e5bdb1c94ec3a61a5f70025cb7baa3 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 10:59:46 +0200 Subject: [PATCH 20/21] . --- packages/nextjs/src/config/getBuildPluginOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 873a28b24128..c676fadd7d0c 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -48,7 +48,7 @@ type BuildTool = keyof typeof LOGGER_PREFIXES; * Normalizes Windows paths to POSIX format for glob patterns */ function normalizePathForGlob(distPath: string): string { - return distPath.replaceAll('\\', '/'); + return distPath.replace(/\\/g, '/'); } /** From 0b42fbfd66e32c6823f28c334de7034df982466f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 19 Sep 2025 11:01:00 +0200 Subject: [PATCH 21/21] reuse normalize func --- packages/nextjs/src/config/getBuildPluginOptions.ts | 2 +- packages/nextjs/src/config/webpack.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index c676fadd7d0c..6de802917015 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -47,7 +47,7 @@ type BuildTool = keyof typeof LOGGER_PREFIXES; /** * Normalizes Windows paths to POSIX format for glob patterns */ -function normalizePathForGlob(distPath: string): string { +export function normalizePathForGlob(distPath: string): string { return distPath.replace(/\\/g, '/'); } diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 93e0dc525401..6ba07cd09f8f 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { sync as resolveSync } from 'resolve'; import type { VercelCronsConfig } from '../common/types'; -import { getBuildPluginOptions } from './getBuildPluginOptions'; +import { getBuildPluginOptions, normalizePathForGlob } from './getBuildPluginOptions'; import type { RouteManifest } from './manifest/types'; // Note: If you need to import a type from Webpack, do it in `types.ts` and export it from there. Otherwise, our // circular dependency check thinks this file is importing from itself. See https://github.com/pahen/madge/issues/306. @@ -418,8 +418,8 @@ export function constructWebpackConfigFunction({ newConfig.plugins = newConfig.plugins || []; const { config: userNextConfig, dir, nextRuntime } = buildContext; const buildTool = isServer ? (nextRuntime === 'edge' ? 'webpack-edge' : 'webpack-nodejs') : 'webpack-client'; - const projectDir = dir.replace(/\\/g, '/'); - const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; + const projectDir = normalizePathForGlob(dir); + const distDir = normalizePathForGlob((userNextConfig as NextConfigObject).distDir ?? '.next'); const distDirAbsPath = path.posix.join(projectDir, distDir); const sentryWebpackPluginInstance = sentryWebpackPlugin(