Skip to content

Commit 049e6e7

Browse files
authored
fix(nextjs): Move userNextConfig.sentry to closure (#5473)
As of vercel/next.js#38498, nextjs now warns (voluminously) if the config exported from `next.config.js` contains properties it doesn't recognize, including the `sentry` property we've been having people use to do things like [disable the webpack plugin](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#disable-sentrywebpackplugin). In order to prevent these warnings, the final config returned from `withSentryConfig` must not have such a property, so this deletes the property from the `userNextConfig` object before returning it. If we were to stop there, the warning would be gone, but so would the data `userNextConfig.sentry` contains. (This is true even if we run `constructWebpackConfigFunction` before doing the deletion, because the place we need the `userNextConfig.sentry` data is not in `constructWebpackConfigFunction` itself but in the function it returns. By the time that function is called by nextjs, the deletion will already have gone through and the data will be gone unless we keep a reference to it elsewhere.) Therefore, in order to allow the returned function to retain access the `userNextConfig.sentry` data, this captures it before it's deleted and passes it to `constructWebpackConfigFunction`, where it lives in a closure around the returned function.
1 parent 22df479 commit 049e6e7

File tree

4 files changed

+58
-30
lines changed

4 files changed

+58
-30
lines changed

packages/nextjs/src/config/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,33 @@ export function withSentryConfig(
1717
if (typeof userNextConfig === 'function') {
1818
return function (phase: string, defaults: { defaultConfig: NextConfigObject }): Partial<NextConfigObject> {
1919
const materializedUserNextConfig = userNextConfig(phase, defaults);
20+
21+
// Next 12.2.3+ warns about non-canonical properties on `userNextConfig`, so grab and then remove the `sentry`
22+
// property there. Where we actually need it is in the webpack config function we're going to create, so pass it
23+
// to `constructWebpackConfigFunction` so that it will be in the created function's closure.
24+
const { sentry: userSentryOptions } = materializedUserNextConfig;
25+
delete materializedUserNextConfig.sentry;
26+
2027
return {
2128
...materializedUserNextConfig,
22-
webpack: constructWebpackConfigFunction(materializedUserNextConfig, userSentryWebpackPluginOptions),
29+
webpack: constructWebpackConfigFunction(
30+
materializedUserNextConfig,
31+
userSentryWebpackPluginOptions,
32+
userSentryOptions,
33+
),
2334
};
2435
};
2536
}
2637

2738
// Otherwise, we can just merge their config with ours and return an object.
39+
40+
// Prevent nextjs from getting mad about having a non-standard config property in `userNextConfig`. (See note above
41+
// for a more thorough explanation of what we're doing here.)
42+
const { sentry: userSentryOptions } = userNextConfig;
43+
delete userNextConfig.sentry;
44+
2845
return {
2946
...userNextConfig,
30-
webpack: constructWebpackConfigFunction(userNextConfig, userSentryWebpackPluginOptions),
47+
webpack: constructWebpackConfigFunction(userNextConfig, userSentryWebpackPluginOptions, userSentryOptions),
3148
};
3249
}

packages/nextjs/src/config/types.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,29 @@ export type NextConfigObject = {
1717
target: 'server' | 'experimental-serverless-trace';
1818
// the output directory for the built app (defaults to ".next")
1919
distDir: string;
20-
sentry?: {
21-
disableServerWebpackPlugin?: boolean;
22-
disableClientWebpackPlugin?: boolean;
23-
hideSourceMaps?: boolean;
24-
25-
// Force webpack to apply the same transpilation rules to the SDK code as apply to user code. Helpful when targeting
26-
// older browsers which don't support ES6 (or ES6+ features like object spread).
27-
transpileClientSDK?: boolean;
28-
// Upload files from `<distDir>/static/chunks` rather than `<distDir>/static/chunks/pages`. Usually files outside of
29-
// `pages/` only contain third-party code, but in cases where they contain user code, restricting the webpack
30-
// plugin's upload breaks sourcemaps for those user-code-containing files, because it keeps them from being
31-
// uploaded. At the same time, we don't want to widen the scope if we don't have to, because we're guaranteed to end
32-
// up uploading too many files, which is why this defaults to `false`.
33-
widenClientFileUpload?: boolean;
34-
};
20+
sentry?: UserSentryOptions;
3521
} & {
3622
// other `next.config.js` options
3723
[key: string]: unknown;
3824
};
3925

26+
export type UserSentryOptions = {
27+
disableServerWebpackPlugin?: boolean;
28+
disableClientWebpackPlugin?: boolean;
29+
hideSourceMaps?: boolean;
30+
31+
// Force webpack to apply the same transpilation rules to the SDK code as apply to user code. Helpful when targeting
32+
// older browsers which don't support ES6 (or ES6+ features like object spread).
33+
transpileClientSDK?: boolean;
34+
35+
// Upload files from `<distDir>/static/chunks` rather than `<distDir>/static/chunks/pages`. Usually files outside of
36+
// `pages/` only contain third-party code, but in cases where they contain user code, restricting the webpack
37+
// plugin's upload breaks sourcemaps for those user-code-containing files, because it keeps them from being
38+
// uploaded. At the same time, we don't want to widen the scope if we don't have to, because we're guaranteed to end
39+
// up uploading too many files, which is why this defaults to `false`.
40+
widenClientFileUpload?: boolean;
41+
};
42+
4043
export type NextConfigFunction = (
4144
phase: string,
4245
defaults: { defaultConfig: NextConfigObject },

packages/nextjs/src/config/webpack.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
EntryPropertyObject,
1111
NextConfigObject,
1212
SentryWebpackPluginOptions,
13+
UserSentryOptions,
1314
WebpackConfigFunction,
1415
WebpackConfigObject,
1516
WebpackEntryProperty,
@@ -37,6 +38,7 @@ export { SentryWebpackPlugin };
3738
export function constructWebpackConfigFunction(
3839
userNextConfig: Partial<NextConfigObject> = {},
3940
userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions> = {},
41+
userSentryOptions: UserSentryOptions = {},
4042
): WebpackConfigFunction {
4143
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
4244
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
@@ -122,9 +124,7 @@ export function constructWebpackConfigFunction(
122124
// with the `--ignore-scripts` option, this will be blocked and the missing binary will cause an error when users
123125
// try to build their apps.)
124126
ensureCLIBinaryExists() &&
125-
(isServer
126-
? !userNextConfig.sentry?.disableServerWebpackPlugin
127-
: !userNextConfig.sentry?.disableClientWebpackPlugin);
127+
(isServer ? !userSentryOptions.disableServerWebpackPlugin : !userSentryOptions.disableClientWebpackPlugin);
128128

129129
if (enableWebpackPlugin) {
130130
// TODO Handle possibility that user is using `SourceMapDevToolPlugin` (see
@@ -138,12 +138,14 @@ export function constructWebpackConfigFunction(
138138
// the browser won't look for them and throw errors into the console when it can't find them. Because this is a
139139
// front-end-only problem, and because `sentry-cli` handles sourcemaps more reliably with the comment than
140140
// without, the option to use `hidden-source-map` only applies to the client-side build.
141-
newConfig.devtool = userNextConfig.sentry?.hideSourceMaps && !isServer ? 'hidden-source-map' : 'source-map';
141+
newConfig.devtool = userSentryOptions.hideSourceMaps && !isServer ? 'hidden-source-map' : 'source-map';
142142
}
143143

144144
newConfig.plugins = newConfig.plugins || [];
145145
newConfig.plugins.push(
146-
new SentryWebpackPlugin(getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions)),
146+
new SentryWebpackPlugin(
147+
getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions, userSentryOptions),
148+
),
147149
);
148150
}
149151

@@ -381,6 +383,7 @@ function shouldAddSentryToEntryPoint(entryPointName: string, isServer: boolean):
381383
export function getWebpackPluginOptions(
382384
buildContext: BuildContext,
383385
userPluginOptions: Partial<SentryWebpackPluginOptions>,
386+
userSentryOptions: UserSentryOptions,
384387
): SentryWebpackPluginOptions {
385388
const { buildId, isServer, webpack, config: userNextConfig, dev: isDev, dir: projectDir } = buildContext;
386389
const distDir = userNextConfig.distDir ?? '.next'; // `.next` is the default directory
@@ -396,14 +399,14 @@ export function getWebpackPluginOptions(
396399
isWebpack5 ? [{ paths: [`${distDir}/server/chunks/`], urlPrefix: `${urlPrefix}/server/chunks` }] : [],
397400
);
398401

399-
const clientInclude = userNextConfig.sentry?.widenClientFileUpload
402+
const clientInclude = userSentryOptions.widenClientFileUpload
400403
? [{ paths: [`${distDir}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }]
401404
: [{ paths: [`${distDir}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` }];
402405

403406
const defaultPluginOptions = dropUndefinedKeys({
404407
include: isServer ? serverInclude : clientInclude,
405408
ignore:
406-
isServer || !userNextConfig.sentry?.widenClientFileUpload
409+
isServer || !userSentryOptions.widenClientFileUpload
407410
? []
408411
: // Widening the upload scope is necessarily going to lead to us uploading files we don't need to (ones which
409412
// don't include any user code). In order to lessen that where we can, exclude the internal nextjs files we know

packages/nextjs/test/config.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ async function materializeFinalWebpackConfig(options: {
200200
const webpackConfigFunction = constructWebpackConfigFunction(
201201
materializedUserNextConfig,
202202
userSentryWebpackPluginConfig,
203+
materializedUserNextConfig.sentry,
203204
);
204205

205206
// call it to get concrete values for comparison
@@ -870,9 +871,11 @@ describe('Sentry webpack plugin config', () => {
870871
[getBuildContext('server', {}, '4'), '.next'],
871872
[getBuildContext('server', {}, '5'), '.next'],
872873
])('`distDir` is not defined', (buildContext: BuildContext, expectedDistDir) => {
873-
const includePaths = getWebpackPluginOptions(buildContext, {
874-
/** userPluginOptions */
875-
}).include as { paths: [] }[];
874+
const includePaths = getWebpackPluginOptions(
875+
buildContext,
876+
{}, // userPluginOptions
877+
{}, // userSentryOptions
878+
).include as { paths: [] }[];
876879

877880
for (const pathDescriptor of includePaths) {
878881
for (const path of pathDescriptor.paths) {
@@ -887,9 +890,11 @@ describe('Sentry webpack plugin config', () => {
887890
[getBuildContext('server', { distDir: 'tmpDir' }, '4'), 'tmpDir'],
888891
[getBuildContext('server', { distDir: 'tmpDir' }, '5'), 'tmpDir'],
889892
])('`distDir` is defined', (buildContext: BuildContext, expectedDistDir) => {
890-
const includePaths = getWebpackPluginOptions(buildContext, {
891-
/** userPluginOptions */
892-
}).include as { paths: [] }[];
893+
const includePaths = getWebpackPluginOptions(
894+
buildContext,
895+
{}, // userPluginOptions
896+
{}, // userSentryOptions
897+
).include as { paths: [] }[];
893898

894899
for (const pathDescriptor of includePaths) {
895900
for (const path of pathDescriptor.paths) {

0 commit comments

Comments
 (0)