Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 252 additions & 49 deletions packages/nextjs/src/config/getBuildPluginOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,264 @@ 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
// 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/**',
PATH: 'server',
},
SERVERLESS: 'serverless/**',
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.*',
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;

/**
* Normalizes Windows paths to POSIX format for glob patterns
*/
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;
}

/**
* Get Sentry Build Plugin options for the runAfterProductionCompile hook.
* Creates file patterns for source map uploads based on build tool and options
*/
function createSourcemapUploadAssetPatterns(
normalizedDistPath: string,
buildTool: BuildTool,
widenClientFileUpload: boolean = false,
): string[] {
const assets: string[] = [];

if (buildTool.startsWith('after-production-compile')) {
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
if (widenClientFileUpload) {
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true })));
} else {
assets.push(
path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: true })),
path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: true })),
);
}
}
} else {
if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') {
// Server builds
assets.push(
path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: false })),
path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS),
);
} else if (buildTool === 'webpack-client') {
// Client builds
if (widenClientFileUpload) {
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: false })));
} else {
assets.push(
path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: false })),
path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: false })),
);
}
}
}

return assets;
}

/**
* Creates ignore patterns for source map uploads
*/
function createSourcemapUploadIgnorePattern(
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 createFilesToDeleteAfterUploadPattern(
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;
}

// 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 {
return source.replace(/^webpack:\/\/(?:_N_E\/)?/, '');
}

/**
* 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 {
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, '/');
const normalizedDistDirAbsPath = normalizePathForGlob(distDirAbsPath);

sourcemapUploadAssets.push(
path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output
const loggerPrefix = LOGGER_PREFIXES[buildTool];
const widenClientFileUpload = sentryBuildOptions.widenClientFileUpload ?? false;
const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false;

const sourcemapUploadAssets = createSourcemapUploadAssetPatterns(
normalizedDistDirAbsPath,
buildTool,
widenClientFileUpload,
);
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 sourcemapUploadIgnore = createSourcemapUploadIgnorePattern(normalizedDistDirAbsPath, widenClientFileUpload);

const filesToDeleteAfterUpload = createFilesToDeleteAfterUploadPattern(
normalizedDistDirAbsPath,
buildTool,
deleteSourcemapsAfterUpload,
useRunAfterProductionCompileHook,
);

const skipSourcemapsUpload = shouldSkipSourcemapUpload(buildTool, useRunAfterProductionCompileHook);

return {
authToken: sentryBuildOptions.authToken,
Expand All @@ -43,51 +269,28 @@ export function getBuildPluginOptions({
telemetry: sentryBuildOptions.telemetry,
debug: sentryBuildOptions.debug,
errorHandler: sentryBuildOptions.errorHandler,
reactComponentAnnotation: {
...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: {
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;
}
},
disable: skipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false),
rewriteSources: rewriteWebpackSources,
assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets,
ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore,
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,
},
_metaOptions: {
loggerPrefixOverride: '[@sentry/nextjs]',
loggerPrefixOverride: loggerPrefix,
telemetry: {
metaFramework: 'nextjs',
},
Expand Down
28 changes: 11 additions & 17 deletions packages/nextjs/src/config/handleRunAfterProductionCompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ export async function handleRunAfterProductionCompile(
{ releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' },
sentryBuildOptions: SentryBuildOptions,
): Promise<void> {
// 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.');
Expand All @@ -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-${buildTool}`,
});

const sentryBuildPluginManager = createSentryBuildPluginManager(options, {
buildTool,
loggerPrefix: '[@sentry/nextjs - After Production Compile]',
});

await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
await sentryBuildPluginManager.createRelease();
Expand Down
Loading