Skip to content

Commit 9b22c8a

Browse files
authored
feat(nextjs): Use afterProductionCompile hook for webpack builds (#17655)
1 parent 65fd3b6 commit 9b22c8a

11 files changed

+811
-485
lines changed

packages/nextjs/src/config/getBuildPluginOptions.ts

Lines changed: 252 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,264 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin
22
import * as path from 'path';
33
import type { SentryBuildOptions } from './types';
44

5+
const LOGGER_PREFIXES = {
6+
'webpack-nodejs': '[@sentry/nextjs - Node.js]',
7+
'webpack-edge': '[@sentry/nextjs - Edge]',
8+
'webpack-client': '[@sentry/nextjs - Client]',
9+
'after-production-compile-webpack': '[@sentry/nextjs - After Production Compile (Webpack)]',
10+
'after-production-compile-turbopack': '[@sentry/nextjs - After Production Compile (Turbopack)]',
11+
} as const;
12+
13+
// File patterns for source map operations
14+
// We use both glob patterns and directory paths for the sourcemap upload and deletion
15+
// -> Direct CLI invocation handles file paths better than glob patterns
16+
// -> Webpack/Bundler needs glob patterns as this is the format that is used by the plugin
17+
const FILE_PATTERNS = {
18+
SERVER: {
19+
GLOB: 'server/**',
20+
PATH: 'server',
21+
},
22+
SERVERLESS: 'serverless/**',
23+
STATIC_CHUNKS: {
24+
GLOB: 'static/chunks/**',
25+
PATH: 'static/chunks',
26+
},
27+
STATIC_CHUNKS_PAGES: {
28+
GLOB: 'static/chunks/pages/**',
29+
PATH: 'static/chunks/pages',
30+
},
31+
STATIC_CHUNKS_APP: {
32+
GLOB: 'static/chunks/app/**',
33+
PATH: 'static/chunks/app',
34+
},
35+
MAIN_CHUNKS: 'static/chunks/main-*',
36+
FRAMEWORK_CHUNKS: 'static/chunks/framework-*',
37+
FRAMEWORK_CHUNKS_DOT: 'static/chunks/framework.*',
38+
POLYFILLS_CHUNKS: 'static/chunks/polyfills-*',
39+
WEBPACK_CHUNKS: 'static/chunks/webpack-*',
40+
} as const;
41+
42+
// Source map file extensions to delete
43+
const SOURCEMAP_EXTENSIONS = ['*.js.map', '*.mjs.map', '*.cjs.map'] as const;
44+
45+
type BuildTool = keyof typeof LOGGER_PREFIXES;
46+
47+
/**
48+
* Normalizes Windows paths to POSIX format for glob patterns
49+
*/
50+
export function normalizePathForGlob(distPath: string): string {
51+
return distPath.replace(/\\/g, '/');
52+
}
53+
54+
/**
55+
* These functions are used to get the correct pattern for the sourcemap upload based on the build tool and the usage context
56+
* -> Direct CLI invocation handles file paths better than glob patterns
57+
*/
58+
function getServerPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string {
59+
return useDirectoryPath ? FILE_PATTERNS.SERVER.PATH : FILE_PATTERNS.SERVER.GLOB;
60+
}
61+
62+
function getStaticChunksPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string {
63+
return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS.PATH : FILE_PATTERNS.STATIC_CHUNKS.GLOB;
64+
}
65+
66+
function getStaticChunksPagesPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string {
67+
return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_PAGES.PATH : FILE_PATTERNS.STATIC_CHUNKS_PAGES.GLOB;
68+
}
69+
70+
function getStaticChunksAppPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string {
71+
return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_APP.PATH : FILE_PATTERNS.STATIC_CHUNKS_APP.GLOB;
72+
}
73+
574
/**
6-
* Get Sentry Build Plugin options for the runAfterProductionCompile hook.
75+
* Creates file patterns for source map uploads based on build tool and options
76+
*/
77+
function createSourcemapUploadAssetPatterns(
78+
normalizedDistPath: string,
79+
buildTool: BuildTool,
80+
widenClientFileUpload: boolean = false,
81+
): string[] {
82+
const assets: string[] = [];
83+
84+
if (buildTool.startsWith('after-production-compile')) {
85+
assets.push(path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: true })));
86+
87+
if (buildTool === 'after-production-compile-turbopack') {
88+
// In turbopack we always want to upload the full static chunks directory
89+
// as the build output is not split into pages|app chunks
90+
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true })));
91+
} else {
92+
// Webpack client builds in after-production-compile mode
93+
if (widenClientFileUpload) {
94+
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true })));
95+
} else {
96+
assets.push(
97+
path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: true })),
98+
path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: true })),
99+
);
100+
}
101+
}
102+
} else {
103+
if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') {
104+
// Server builds
105+
assets.push(
106+
path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: false })),
107+
path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS),
108+
);
109+
} else if (buildTool === 'webpack-client') {
110+
// Client builds
111+
if (widenClientFileUpload) {
112+
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: false })));
113+
} else {
114+
assets.push(
115+
path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: false })),
116+
path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: false })),
117+
);
118+
}
119+
}
120+
}
121+
122+
return assets;
123+
}
124+
125+
/**
126+
* Creates ignore patterns for source map uploads
127+
*/
128+
function createSourcemapUploadIgnorePattern(
129+
normalizedDistPath: string,
130+
widenClientFileUpload: boolean = false,
131+
): string[] {
132+
const ignore: string[] = [];
133+
134+
// We only add main-* files if the user has not opted into it
135+
if (!widenClientFileUpload) {
136+
ignore.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.MAIN_CHUNKS));
137+
}
138+
139+
// Always ignore these patterns
140+
ignore.push(
141+
path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS),
142+
path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS_DOT),
143+
path.posix.join(normalizedDistPath, FILE_PATTERNS.POLYFILLS_CHUNKS),
144+
path.posix.join(normalizedDistPath, FILE_PATTERNS.WEBPACK_CHUNKS),
145+
);
146+
147+
return ignore;
148+
}
149+
150+
/**
151+
* Creates file patterns for deletion after source map upload
152+
*/
153+
function createFilesToDeleteAfterUploadPattern(
154+
normalizedDistPath: string,
155+
buildTool: BuildTool,
156+
deleteSourcemapsAfterUpload: boolean,
157+
useRunAfterProductionCompileHook: boolean = false,
158+
): string[] | undefined {
159+
if (!deleteSourcemapsAfterUpload) {
160+
return undefined;
161+
}
162+
163+
// We don't want to delete source maps for server builds as this led to errors on Vercel in the past
164+
// See: https://github.com/getsentry/sentry-javascript/issues/13099
165+
if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') {
166+
return undefined;
167+
}
168+
169+
// Skip deletion for webpack client builds when using the experimental hook
170+
if (buildTool === 'webpack-client' && useRunAfterProductionCompileHook) {
171+
return undefined;
172+
}
173+
174+
return SOURCEMAP_EXTENSIONS.map(ext => path.posix.join(normalizedDistPath, 'static', '**', ext));
175+
}
176+
177+
/**
178+
* Determines if sourcemap uploads should be skipped
179+
*/
180+
function shouldSkipSourcemapUpload(buildTool: BuildTool, useRunAfterProductionCompileHook: boolean = false): boolean {
181+
return useRunAfterProductionCompileHook && buildTool.startsWith('webpack');
182+
}
183+
184+
/**
185+
* Source rewriting function for webpack sources
186+
*/
187+
function rewriteWebpackSources(source: string): string {
188+
return source.replace(/^webpack:\/\/(?:_N_E\/)?/, '');
189+
}
190+
191+
/**
192+
* Creates release configuration
193+
*/
194+
function createReleaseConfig(
195+
releaseName: string | undefined,
196+
sentryBuildOptions: SentryBuildOptions,
197+
): SentryBuildPluginOptions['release'] {
198+
if (releaseName !== undefined) {
199+
return {
200+
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
201+
name: releaseName,
202+
create: sentryBuildOptions.release?.create,
203+
finalize: sentryBuildOptions.release?.finalize,
204+
dist: sentryBuildOptions.release?.dist,
205+
vcsRemote: sentryBuildOptions.release?.vcsRemote,
206+
setCommits: sentryBuildOptions.release?.setCommits,
207+
deploy: sentryBuildOptions.release?.deploy,
208+
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
209+
};
210+
}
211+
212+
return {
213+
inject: false,
214+
create: false,
215+
finalize: false,
216+
};
217+
}
218+
219+
/**
220+
* Get Sentry Build Plugin options for both webpack and turbopack builds.
221+
* These options can be used in two ways:
222+
* 1. The options can be built in a single operation after the production build completes
223+
* 2. The options can be built in multiple operations, one for each webpack build
7224
*/
8225
export function getBuildPluginOptions({
9226
sentryBuildOptions,
10227
releaseName,
11228
distDirAbsPath,
229+
buildTool,
230+
useRunAfterProductionCompileHook,
12231
}: {
13232
sentryBuildOptions: SentryBuildOptions;
14233
releaseName: string | undefined;
15234
distDirAbsPath: string;
235+
buildTool: BuildTool;
236+
useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook
16237
}): SentryBuildPluginOptions {
17-
const sourcemapUploadAssets: string[] = [];
18-
const sourcemapUploadIgnore: string[] = [];
19-
20-
const filesToDeleteAfterUpload: string[] = [];
21-
22238
// We need to convert paths to posix because Glob patterns use `\` to escape
23239
// glob characters. This clashes with Windows path separators.
24240
// See: https://www.npmjs.com/package/glob
25-
const normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/');
241+
const normalizedDistDirAbsPath = normalizePathForGlob(distDirAbsPath);
26242

27-
sourcemapUploadAssets.push(
28-
path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output
243+
const loggerPrefix = LOGGER_PREFIXES[buildTool];
244+
const widenClientFileUpload = sentryBuildOptions.widenClientFileUpload ?? false;
245+
const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false;
246+
247+
const sourcemapUploadAssets = createSourcemapUploadAssetPatterns(
248+
normalizedDistDirAbsPath,
249+
buildTool,
250+
widenClientFileUpload,
29251
);
30-
if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) {
31-
filesToDeleteAfterUpload.push(
32-
path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'),
33-
path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'),
34-
path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'),
35-
);
36-
}
252+
253+
const sourcemapUploadIgnore = createSourcemapUploadIgnorePattern(normalizedDistDirAbsPath, widenClientFileUpload);
254+
255+
const filesToDeleteAfterUpload = createFilesToDeleteAfterUploadPattern(
256+
normalizedDistDirAbsPath,
257+
buildTool,
258+
deleteSourcemapsAfterUpload,
259+
useRunAfterProductionCompileHook,
260+
);
261+
262+
const skipSourcemapsUpload = shouldSkipSourcemapUpload(buildTool, useRunAfterProductionCompileHook);
37263

38264
return {
39265
authToken: sentryBuildOptions.authToken,
@@ -43,51 +269,28 @@ export function getBuildPluginOptions({
43269
telemetry: sentryBuildOptions.telemetry,
44270
debug: sentryBuildOptions.debug,
45271
errorHandler: sentryBuildOptions.errorHandler,
46-
reactComponentAnnotation: {
47-
...sentryBuildOptions.reactComponentAnnotation,
48-
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation,
49-
},
272+
reactComponentAnnotation: buildTool.startsWith('after-production-compile')
273+
? undefined
274+
: {
275+
...sentryBuildOptions.reactComponentAnnotation,
276+
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation,
277+
},
50278
silent: sentryBuildOptions.silent,
51279
url: sentryBuildOptions.sentryUrl,
52280
sourcemaps: {
53-
disable: sentryBuildOptions.sourcemaps?.disable,
54-
rewriteSources(source) {
55-
if (source.startsWith('webpack://_N_E/')) {
56-
return source.replace('webpack://_N_E/', '');
57-
} else if (source.startsWith('webpack://')) {
58-
return source.replace('webpack://', '');
59-
} else {
60-
return source;
61-
}
62-
},
281+
disable: skipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false),
282+
rewriteSources: rewriteWebpackSources,
63283
assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets,
64284
ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore,
65285
filesToDeleteAfterUpload,
66286
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
67287
},
68-
release:
69-
releaseName !== undefined
70-
? {
71-
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
72-
name: releaseName,
73-
create: sentryBuildOptions.release?.create,
74-
finalize: sentryBuildOptions.release?.finalize,
75-
dist: sentryBuildOptions.release?.dist,
76-
vcsRemote: sentryBuildOptions.release?.vcsRemote,
77-
setCommits: sentryBuildOptions.release?.setCommits,
78-
deploy: sentryBuildOptions.release?.deploy,
79-
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
80-
}
81-
: {
82-
inject: false,
83-
create: false,
84-
finalize: false,
85-
},
288+
release: createReleaseConfig(releaseName, sentryBuildOptions),
86289
bundleSizeOptimizations: {
87290
...sentryBuildOptions.bundleSizeOptimizations,
88291
},
89292
_metaOptions: {
90-
loggerPrefixOverride: '[@sentry/nextjs]',
293+
loggerPrefixOverride: loggerPrefix,
91294
telemetry: {
92295
metaFramework: 'nextjs',
93296
},

packages/nextjs/src/config/handleRunAfterProductionCompile.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ export async function handleRunAfterProductionCompile(
1111
{ releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' },
1212
sentryBuildOptions: SentryBuildOptions,
1313
): Promise<void> {
14-
// We don't want to do anything for webpack at this point because the plugin already handles this
15-
// TODO: Actually implement this for webpack as well
16-
if (buildTool === 'webpack') {
17-
return;
18-
}
19-
2014
if (sentryBuildOptions.debug) {
2115
// eslint-disable-next-line no-console
2216
console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.');
@@ -36,17 +30,17 @@ export async function handleRunAfterProductionCompile(
3630
return;
3731
}
3832

39-
const sentryBuildPluginManager = createSentryBuildPluginManager(
40-
getBuildPluginOptions({
41-
sentryBuildOptions,
42-
releaseName,
43-
distDirAbsPath: distDir,
44-
}),
45-
{
46-
buildTool,
47-
loggerPrefix: '[@sentry/nextjs]',
48-
},
49-
);
33+
const options = getBuildPluginOptions({
34+
sentryBuildOptions,
35+
releaseName,
36+
distDirAbsPath: distDir,
37+
buildTool: `after-production-compile-${buildTool}`,
38+
});
39+
40+
const sentryBuildPluginManager = createSentryBuildPluginManager(options, {
41+
buildTool,
42+
loggerPrefix: '[@sentry/nextjs - After Production Compile]',
43+
});
5044

5145
await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
5246
await sentryBuildPluginManager.createRelease();

0 commit comments

Comments
 (0)