|
| 1 | +import { getSentryRelease } from '@sentry/node'; |
| 2 | +import { dropUndefinedKeys, logger } from '@sentry/utils'; |
| 3 | +import * as SentryWebpackPlugin from '@sentry/webpack-plugin'; |
| 4 | + |
| 5 | +import { |
| 6 | + BuildContext, |
| 7 | + EntryPropertyObject, |
| 8 | + ExportedNextConfig, |
| 9 | + SentryWebpackPluginOptions, |
| 10 | + WebpackConfigFunction, |
| 11 | + WebpackConfigObject, |
| 12 | + WebpackEntryProperty, |
| 13 | +} from './types'; |
| 14 | +import { |
| 15 | + SENTRY_CLIENT_CONFIG_FILE, |
| 16 | + SENTRY_SERVER_CONFIG_FILE, |
| 17 | + SERVER_SDK_INIT_PATH, |
| 18 | + storeServerConfigFileLocation, |
| 19 | +} from './utils'; |
| 20 | + |
| 21 | +export { SentryWebpackPlugin }; |
| 22 | + |
| 23 | +// TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile |
| 24 | +// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include |
| 25 | +// TODO: drop merged keys from override check? `includeDefaults` option? |
| 26 | + |
| 27 | +const defaultSentryWebpackPluginOptions = dropUndefinedKeys({ |
| 28 | + url: process.env.SENTRY_URL, |
| 29 | + org: process.env.SENTRY_ORG, |
| 30 | + project: process.env.SENTRY_PROJECT, |
| 31 | + authToken: process.env.SENTRY_AUTH_TOKEN, |
| 32 | + configFile: 'sentry.properties', |
| 33 | + stripPrefix: ['webpack://_N_E/'], |
| 34 | + urlPrefix: `~/_next`, |
| 35 | + include: '.next/', |
| 36 | + ignore: ['.next/cache', 'server/ssr-module-cache.js', 'static/*/_ssgManifest.js', 'static/*/_buildManifest.js'], |
| 37 | +}); |
| 38 | + |
| 39 | +/** |
| 40 | + * Construct the function which will be used as the nextjs config's `webpack` value. |
| 41 | + * |
| 42 | + * Sets: |
| 43 | + * - `devtool`, to ensure high-quality sourcemaps are generated |
| 44 | + * - `entry`, to include user's sentry config files (where `Sentry.init` is called) in the build |
| 45 | + * - `plugins`, to add SentryWebpackPlugin (TODO: optional) |
| 46 | + * |
| 47 | + * @param userNextConfig The user's existing nextjs config, as passed to `withSentryConfig` |
| 48 | + * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` |
| 49 | + * @returns The function to set as the nextjs config's `webpack` value |
| 50 | + */ |
| 51 | +export function constructWebpackConfigFunction( |
| 52 | + userNextConfig: ExportedNextConfig = {}, |
| 53 | + userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions> = {}, |
| 54 | +): WebpackConfigFunction { |
| 55 | + const newWebpackFunction = (config: WebpackConfigObject, options: BuildContext): WebpackConfigObject => { |
| 56 | + // if we're building server code, store the webpack output path as an env variable, so we know where to look for the |
| 57 | + // webpack-processed version of `sentry.server.config.js` when we need it |
| 58 | + if (config.target === 'node') { |
| 59 | + storeServerConfigFileLocation(config); |
| 60 | + } |
| 61 | + |
| 62 | + let newConfig = config; |
| 63 | + |
| 64 | + // if user has custom webpack config (which always takes the form of a function), run it so we have actual values to |
| 65 | + // work with |
| 66 | + if ('webpack' in userNextConfig && typeof userNextConfig.webpack === 'function') { |
| 67 | + newConfig = userNextConfig.webpack(config, options); |
| 68 | + } |
| 69 | + |
| 70 | + // Ensure quality source maps in production. (Source maps aren't uploaded in dev, and besides, Next doesn't let you |
| 71 | + // change this is dev even if you want to - see |
| 72 | + // https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md.) |
| 73 | + if (!options.dev) { |
| 74 | + // TODO Handle possibility that user is using `SourceMapDevToolPlugin` (see |
| 75 | + // https://webpack.js.org/plugins/source-map-dev-tool-plugin/) |
| 76 | + // TODO Give user option to use `hidden-source-map` ? |
| 77 | + newConfig.devtool = 'source-map'; |
| 78 | + } |
| 79 | + |
| 80 | + // Tell webpack to inject user config files (containing the two `Sentry.init()` calls) into the appropriate output |
| 81 | + // bundles. Store a separate reference to the original `entry` value to avoid an infinite loop. (If we don't do |
| 82 | + // this, we'll have a statement of the form `x.y = () => f(x.y)`, where one of the things `f` does is call `x.y`. |
| 83 | + // Since we're setting `x.y` to be a callback (which, by definition, won't run until some time later), by the time |
| 84 | + // the function runs (causing `f` to run, causing `x.y` to run), `x.y` will point to the callback itself, rather |
| 85 | + // than its original value. So calling it will call the callback which will call `f` which will call `x.y` which |
| 86 | + // will call the callback which will call `f` which will call `x.y`... and on and on. Theoretically this could also |
| 87 | + // be fixed by using `bind`, but this is way simpler.) |
| 88 | + const origEntryProperty = newConfig.entry; |
| 89 | + newConfig.entry = async () => addSentryToEntryProperty(origEntryProperty, options.isServer); |
| 90 | + |
| 91 | + // Add the Sentry plugin, which uploads source maps to Sentry when not in dev |
| 92 | + checkWebpackPluginOverrides(userSentryWebpackPluginOptions); |
| 93 | + newConfig.plugins = newConfig.plugins || []; |
| 94 | + newConfig.plugins.push( |
| 95 | + // @ts-ignore Our types for the plugin are messed up somehow - TS wants this to be `SentryWebpackPlugin.default`, |
| 96 | + // but that's not actually a thing |
| 97 | + new SentryWebpackPlugin({ |
| 98 | + dryRun: options.dev, |
| 99 | + release: getSentryRelease(options.buildId), |
| 100 | + ...defaultSentryWebpackPluginOptions, |
| 101 | + ...userSentryWebpackPluginOptions, |
| 102 | + }), |
| 103 | + ); |
| 104 | + |
| 105 | + return newConfig; |
| 106 | + }; |
| 107 | + |
| 108 | + return newWebpackFunction; |
| 109 | +} |
| 110 | + |
| 111 | +/** |
| 112 | + * Modify the webpack `entry` property so that the code in `sentry.server.config.js` and `sentry.client.config.js` is |
| 113 | + * included in the the necessary bundles. |
| 114 | + * |
| 115 | + * @param origEntryProperty The value of the property before Sentry code has been injected |
| 116 | + * @param isServer A boolean provided by nextjs indicating whether we're handling the server bundles or the browser |
| 117 | + * bundles |
| 118 | + * @returns The value which the new `entry` property (which will be a function) will return (TODO: this should return |
| 119 | + * the function, rather than the function's return value) |
| 120 | + */ |
| 121 | +async function addSentryToEntryProperty( |
| 122 | + origEntryProperty: WebpackEntryProperty, |
| 123 | + isServer: boolean, |
| 124 | +): Promise<EntryPropertyObject> { |
| 125 | + // The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs |
| 126 | + // sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether |
| 127 | + // someone else has come along before us and changed that, we need to check a few things along the way. The one thing |
| 128 | + // we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function |
| 129 | + // options. See https://webpack.js.org/configuration/entry-context/#entry. |
| 130 | + |
| 131 | + let newEntryProperty = origEntryProperty; |
| 132 | + if (typeof origEntryProperty === 'function') { |
| 133 | + newEntryProperty = await origEntryProperty(); |
| 134 | + } |
| 135 | + newEntryProperty = newEntryProperty as EntryPropertyObject; |
| 136 | + |
| 137 | + // Add a new element to the `entry` array, we force webpack to create a bundle out of the user's |
| 138 | + // `sentry.server.config.js` file and output it to `SERVER_INIT_LOCATION`. (See |
| 139 | + // https://webpack.js.org/guides/code-splitting/#entry-points.) We do this so that the user's config file is run |
| 140 | + // through babel (and any other processors through which next runs the rest of the user-provided code - pages, API |
| 141 | + // routes, etc.). Specifically, we need any ESM-style `import` code to get transpiled into ES5, so that we can call |
| 142 | + // `require()` on the resulting file when we're instrumenting the sesrver. (We can't use a dynamic import there |
| 143 | + // because that then forces the user into a particular TS config.) |
| 144 | + |
| 145 | + // On the server, create a separate bundle, as there's no one entry point depended on by all the others |
| 146 | + if (isServer) { |
| 147 | + // slice off the final `.js` since webpack is going to add it back in for us, and we don't want to end up with |
| 148 | + // `.js.js` as the extension |
| 149 | + newEntryProperty[SERVER_SDK_INIT_PATH.slice(0, -3)] = SENTRY_SERVER_CONFIG_FILE; |
| 150 | + } |
| 151 | + // On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page. |
| 152 | + else { |
| 153 | + addFileToExistingEntryPoint(newEntryProperty, 'main', SENTRY_CLIENT_CONFIG_FILE); |
| 154 | + } |
| 155 | + |
| 156 | + return newEntryProperty; |
| 157 | +} |
| 158 | + |
| 159 | +/** |
| 160 | + * Add a file to a specific element of the given `entry` webpack config property. |
| 161 | + * |
| 162 | + * @param entryProperty The existing `entry` config object |
| 163 | + * @param entryPointName The key where the file should be injected |
| 164 | + * @param filepath The path to the injected file |
| 165 | + */ |
| 166 | +function addFileToExistingEntryPoint( |
| 167 | + entryProperty: EntryPropertyObject, |
| 168 | + entryPointName: string, |
| 169 | + filepath: string, |
| 170 | +): void { |
| 171 | + // can be a string, array of strings, or object whose `import` property is one of those two |
| 172 | + let injectedInto = entryProperty[entryPointName]; |
| 173 | + |
| 174 | + // Sometimes especially for older next.js versions it happens we don't have an entry point |
| 175 | + if (!injectedInto) { |
| 176 | + // eslint-disable-next-line no-console |
| 177 | + console.error(`[Sentry] Can't inject ${filepath}, no entrypoint is defined.`); |
| 178 | + return; |
| 179 | + } |
| 180 | + |
| 181 | + // We inject the user's client config file after the existing code so that the config file has access to |
| 182 | + // `publicRuntimeConfig`. See https://github.com/getsentry/sentry-javascript/issues/3485 |
| 183 | + if (typeof injectedInto === 'string') { |
| 184 | + injectedInto = [injectedInto, filepath]; |
| 185 | + } else if (Array.isArray(injectedInto)) { |
| 186 | + injectedInto = [...injectedInto, filepath]; |
| 187 | + } else { |
| 188 | + let importVal: string | string[]; |
| 189 | + |
| 190 | + if (typeof injectedInto.import === 'string') { |
| 191 | + importVal = [injectedInto.import, filepath]; |
| 192 | + } else { |
| 193 | + importVal = [...injectedInto.import, filepath]; |
| 194 | + } |
| 195 | + |
| 196 | + injectedInto = { |
| 197 | + ...injectedInto, |
| 198 | + import: importVal, |
| 199 | + }; |
| 200 | + } |
| 201 | + |
| 202 | + entryProperty[entryPointName] = injectedInto; |
| 203 | +} |
| 204 | + |
| 205 | +/** |
| 206 | + * Check the SentryWebpackPlugin options provided by the user against the options we set by default, and warn if any of |
| 207 | + * our default options are getting overridden. (Note: If any of our default values is undefined, it won't be included in |
| 208 | + * the warning.) |
| 209 | + * |
| 210 | + * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin options |
| 211 | + */ |
| 212 | +function checkWebpackPluginOverrides(userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>): void { |
| 213 | + // warn if any of the default options for the webpack plugin are getting overridden |
| 214 | + const sentryWebpackPluginOptionOverrides = Object.keys(defaultSentryWebpackPluginOptions) |
| 215 | + .concat('dryrun') |
| 216 | + .filter(key => key in userSentryWebpackPluginOptions); |
| 217 | + if (sentryWebpackPluginOptionOverrides.length > 0) { |
| 218 | + logger.warn( |
| 219 | + '[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' + |
| 220 | + `\t${sentryWebpackPluginOptionOverrides.toString()},\n` + |
| 221 | + "which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing.", |
| 222 | + ); |
| 223 | + } |
| 224 | +} |
0 commit comments