diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 26279d19271c..c3f54023347b 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -30,6 +30,8 @@ export type NextConfigObject = { distDir?: string; // The root at which the nextjs app will be served (defaults to "/") basePath?: string; + // The asset prefix (pathname or full URL) if assets will not be stored at the default Next.js location + assetPrefix?: string; // Config which will be available at runtime publicRuntimeConfig?: { [key: string]: unknown }; // File extensions that count as pages in the `pages/` directory diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 81e38cee6007..ed9dbb1cdd18 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -5,6 +5,7 @@ import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin'; import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; +import * as url from 'url'; import { BuildContext, @@ -457,17 +458,32 @@ export function getWebpackPluginOptions( const isWebpack5 = webpack.version.startsWith('5'); const isServerless = userNextConfig.target === 'experimental-serverless-trace'; const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties')); - const urlPrefix = userNextConfig.basePath ? `~${userNextConfig.basePath}/_next` : '~/_next'; + + let basePath = userNextConfig.basePath || ''; + if (basePath) { + basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + } + const serverUrlPrefix = `~${basePath}/_next`; + + let assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; + if (assetPrefix) { + const assertPrefixUrl = url.parse(assetPrefix); + assetPrefix = assertPrefixUrl.pathname || ''; + assetPrefix = assetPrefix.endsWith('/') ? assetPrefix.slice(0, -1) : assetPrefix; + } + const clientUrlPrefix = `~${assetPrefix}/_next`; const serverInclude = isServerless - ? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }] - : [{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${urlPrefix}/server/pages` }].concat( - isWebpack5 ? [{ paths: [`${distDirAbsPath}/server/chunks/`], urlPrefix: `${urlPrefix}/server/chunks` }] : [], + ? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${serverUrlPrefix}/serverless` }] + : [{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${serverUrlPrefix}/server/pages` }].concat( + isWebpack5 + ? [{ paths: [`${distDirAbsPath}/server/chunks/`], urlPrefix: `${serverUrlPrefix}/server/chunks` }] + : [], ); const clientInclude = userSentryOptions.widenClientFileUpload - ? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }] - : [{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` }]; + ? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${clientUrlPrefix}/static/chunks` }] + : [{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${clientUrlPrefix}/static/chunks/pages` }]; const defaultPluginOptions = dropUndefinedKeys({ include: isServer ? serverInclude : clientInclude, @@ -484,7 +500,6 @@ export function getWebpackPluginOptions( authToken: process.env.SENTRY_AUTH_TOKEN, configFile: hasSentryProperties ? 'sentry.properties' : undefined, stripPrefix: ['webpack://_N_E/'], - urlPrefix, entries: (entryPointName: string) => shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes), release: getSentryRelease(buildId), diff --git a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts index e1e16a0e7ed1..63ba018cef55 100644 --- a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts +++ b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts @@ -35,7 +35,6 @@ describe('Sentry webpack plugin config', () => { project: 'simulator', // from user webpack plugin config authToken: 'dogsarebadatkeepingsecrets', // picked up from env stripPrefix: ['webpack://_N_E/'], // default - urlPrefix: '~/_next', // default entries: expect.any(Function), // default, tested separately elsewhere release: 'doGsaREgReaT', // picked up from env dryRun: false, // based on buildContext.dev being false @@ -295,6 +294,161 @@ describe('Sentry webpack plugin config', () => { }); }); + describe('Sentry webpack plugin `includes` option with assetPrefix set', () => { + it('does not affect server build', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: '/asset-prefix', + }; + const serverBuildContextWebpack4 = getBuildContext('server', exportedNextConfigWithAssetPrefix); + serverBuildContextWebpack4.webpack.version = '4.15.13'; + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContextWebpack4, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${serverBuildContextWebpack4.dir}/.next/server/pages/`], + urlPrefix: '~/_next/server/pages', + }, + ]); + }); + + it('has the correct value given a path', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: '/asset-prefix', + }; + const buildContext = getBuildContext('client', exportedNextConfigWithAssetPrefix); + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: buildContext, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${buildContext.dir}/.next/static/chunks/pages`], + urlPrefix: '~/asset-prefix/_next/static/chunks/pages', + }, + ]); + }); + + it('has the correct value given a path with a leading slash', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: '/asset-prefix/', + }; + const buildContext = getBuildContext('client', exportedNextConfigWithAssetPrefix); + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: buildContext, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${buildContext.dir}/.next/static/chunks/pages`], + urlPrefix: '~/asset-prefix/_next/static/chunks/pages', + }, + ]); + }); + + it('has the correct value when given a full URL with no path', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: 'https://cdn.mydomain.com', + }; + const buildContext = getBuildContext('client', exportedNextConfigWithAssetPrefix); + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: buildContext, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${buildContext.dir}/.next/static/chunks/pages`], + urlPrefix: '~/_next/static/chunks/pages', + }, + ]); + }); + + it('has the correct value when given a full URL with a path', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: 'https://cdn.mydomain.com/asset-prefix', + }; + const buildContext = getBuildContext('client', exportedNextConfigWithAssetPrefix); + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: buildContext, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${buildContext.dir}/.next/static/chunks/pages`], + urlPrefix: '~/asset-prefix/_next/static/chunks/pages', + }, + ]); + }); + + it('takes priority over basePath ', async () => { + const exportedNextConfigWithAssetPrefix = { + ...exportedNextConfig, + assetPrefix: '/asset-prefix', + basePath: '/base-path', + }; + const buildContext = getBuildContext('client', exportedNextConfigWithAssetPrefix); + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig: exportedNextConfigWithAssetPrefix, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: buildContext, + }); + + const sentryWebpackPluginInstance = findWebpackPlugin( + finalWebpackConfig, + 'SentryCliPlugin', + ) as SentryWebpackPlugin; + + expect(sentryWebpackPluginInstance.options.include).toEqual([ + { + paths: [`${buildContext.dir}/.next/static/chunks/pages`], + urlPrefix: '~/asset-prefix/_next/static/chunks/pages', + }, + ]); + }); + }); + describe('SentryWebpackPlugin enablement', () => { let processEnvBackup: typeof process.env;