webpack.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. import { _nullishCoalesce, _optionalChain } from '@sentry/utils';
  2. import * as fs from 'fs';
  3. import * as path from 'path';
  4. import { getSentryRelease } from '@sentry/node';
  5. import { escapeStringForRegex, logger, loadModule, arrayify, dropUndefinedKeys } from '@sentry/utils';
  6. import * as chalk from 'chalk';
  7. import { sync } from 'resolve';
  8. import { DEBUG_BUILD } from '../common/debug-build.js';
  9. const RUNTIME_TO_SDK_ENTRYPOINT_MAP = {
  10. client: './client',
  11. server: './server',
  12. edge: './edge',
  13. } ;
  14. // Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain
  15. // warnings 3 times, we keep track of them here.
  16. let showedMissingAuthTokenErrorMsg = false;
  17. let showedMissingOrgSlugErrorMsg = false;
  18. let showedMissingProjectSlugErrorMsg = false;
  19. let showedHiddenSourceMapsWarningMsg = false;
  20. let showedMissingCliBinaryWarningMsg = false;
  21. let showedMissingGlobalErrorWarningMsg = false;
  22. // TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile
  23. // TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
  24. // TODO: drop merged keys from override check? `includeDefaults` option?
  25. /**
  26. * Construct the function which will be used as the nextjs config's `webpack` value.
  27. *
  28. * Sets:
  29. * - `devtool`, to ensure high-quality sourcemaps are generated
  30. * - `entry`, to include user's sentry config files (where `Sentry.init` is called) in the build
  31. * - `plugins`, to add SentryWebpackPlugin
  32. *
  33. * @param userNextConfig The user's existing nextjs config, as passed to `withSentryConfig`
  34. * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig`
  35. * @returns The function to set as the nextjs config's `webpack` value
  36. */
  37. function constructWebpackConfigFunction(
  38. userNextConfig = {},
  39. userSentryWebpackPluginOptions = {},
  40. userSentryOptions = {},
  41. ) {
  42. // Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
  43. // we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
  44. // `incomingConfig` and `buildContext` are referred to as `config` and `options` in the nextjs docs.
  45. return function newWebpackFunction(
  46. incomingConfig,
  47. buildContext,
  48. ) {
  49. const { isServer, dev: isDev, dir: projectDir } = buildContext;
  50. const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'server') : 'client';
  51. let rawNewConfig = { ...incomingConfig };
  52. // if user has custom webpack config (which always takes the form of a function), run it so we have actual values to
  53. // work with
  54. if ('webpack' in userNextConfig && typeof userNextConfig.webpack === 'function') {
  55. rawNewConfig = userNextConfig.webpack(rawNewConfig, buildContext);
  56. }
  57. // This mutates `rawNewConfig` in place, but also returns it in order to switch its type to one in which
  58. // `newConfig.module.rules` is required, so we don't have to keep asserting its existence
  59. const newConfig = setUpModuleRules(rawNewConfig);
  60. // Add a loader which will inject code that sets global values
  61. addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, userSentryWebpackPluginOptions);
  62. newConfig.module.rules.push({
  63. test: /node_modules[/\\]@sentry[/\\]nextjs/,
  64. use: [
  65. {
  66. loader: path.resolve(__dirname, 'loaders', 'sdkMultiplexerLoader.js'),
  67. options: {
  68. importTarget: RUNTIME_TO_SDK_ENTRYPOINT_MAP[runtime],
  69. },
  70. },
  71. ],
  72. });
  73. let pagesDirPath;
  74. const maybePagesDirPath = path.join(projectDir, 'pages');
  75. const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages');
  76. if (fs.existsSync(maybePagesDirPath) && fs.lstatSync(maybePagesDirPath).isDirectory()) {
  77. pagesDirPath = maybePagesDirPath;
  78. } else if (fs.existsSync(maybeSrcPagesDirPath) && fs.lstatSync(maybeSrcPagesDirPath).isDirectory()) {
  79. pagesDirPath = maybeSrcPagesDirPath;
  80. }
  81. let appDirPath;
  82. const maybeAppDirPath = path.join(projectDir, 'app');
  83. const maybeSrcAppDirPath = path.join(projectDir, 'src', 'app');
  84. if (fs.existsSync(maybeAppDirPath) && fs.lstatSync(maybeAppDirPath).isDirectory()) {
  85. appDirPath = maybeAppDirPath;
  86. } else if (fs.existsSync(maybeSrcAppDirPath) && fs.lstatSync(maybeSrcAppDirPath).isDirectory()) {
  87. appDirPath = maybeSrcAppDirPath;
  88. }
  89. const apiRoutesPath = pagesDirPath ? path.join(pagesDirPath, 'api') : undefined;
  90. const middlewareLocationFolder = pagesDirPath
  91. ? path.join(pagesDirPath, '..')
  92. : appDirPath
  93. ? path.join(appDirPath, '..')
  94. : projectDir;
  95. // Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
  96. const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
  97. const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
  98. const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
  99. const staticWrappingLoaderOptions = {
  100. appDir: appDirPath,
  101. pagesDir: pagesDirPath,
  102. pageExtensionRegex,
  103. excludeServerRoutes: userSentryOptions.excludeServerRoutes,
  104. sentryConfigFilePath: getUserConfigFilePath(projectDir, runtime),
  105. nextjsRequestAsyncStorageModulePath: getRequestAsyncStorageModuleLocation(
  106. projectDir,
  107. _optionalChain([rawNewConfig, 'access', _ => _.resolve, 'optionalAccess', _2 => _2.modules]),
  108. ),
  109. };
  110. const normalizeLoaderResourcePath = (resourcePath) => {
  111. // `resourcePath` may be an absolute path or a path relative to the context of the webpack config
  112. let absoluteResourcePath;
  113. if (path.isAbsolute(resourcePath)) {
  114. absoluteResourcePath = resourcePath;
  115. } else {
  116. absoluteResourcePath = path.join(projectDir, resourcePath);
  117. }
  118. return path.normalize(absoluteResourcePath);
  119. };
  120. const isPageResource = (resourcePath) => {
  121. const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
  122. return (
  123. pagesDirPath !== undefined &&
  124. normalizedAbsoluteResourcePath.startsWith(pagesDirPath + path.sep) &&
  125. !normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
  126. dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
  127. );
  128. };
  129. const isApiRouteResource = (resourcePath) => {
  130. const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
  131. return (
  132. normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
  133. dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
  134. );
  135. };
  136. const possibleMiddlewareLocations = ['js', 'jsx', 'ts', 'tsx'].map(middlewareFileEnding => {
  137. return path.join(middlewareLocationFolder, `middleware.${middlewareFileEnding}`);
  138. });
  139. const isMiddlewareResource = (resourcePath) => {
  140. const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
  141. return possibleMiddlewareLocations.includes(normalizedAbsoluteResourcePath);
  142. };
  143. const isServerComponentResource = (resourcePath) => {
  144. const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
  145. // ".js, .jsx, or .tsx file extensions can be used for Pages"
  146. // https://beta.nextjs.org/docs/routing/pages-and-layouts#pages:~:text=.js%2C%20.jsx%2C%20or%20.tsx%20file%20extensions%20can%20be%20used%20for%20Pages.
  147. return (
  148. appDirPath !== undefined &&
  149. normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
  150. !!normalizedAbsoluteResourcePath.match(/[\\/](page|layout|loading|head|not-found)\.(js|jsx|tsx)$/)
  151. );
  152. };
  153. const isRouteHandlerResource = (resourcePath) => {
  154. const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
  155. return (
  156. appDirPath !== undefined &&
  157. normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
  158. !!normalizedAbsoluteResourcePath.match(/[\\/]route\.(js|jsx|ts|tsx)$/)
  159. );
  160. };
  161. if (isServer && userSentryOptions.autoInstrumentServerFunctions !== false) {
  162. // It is very important that we insert our loaders at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened.
  163. // Wrap pages
  164. newConfig.module.rules.unshift({
  165. test: isPageResource,
  166. use: [
  167. {
  168. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  169. options: {
  170. ...staticWrappingLoaderOptions,
  171. wrappingTargetKind: 'page',
  172. },
  173. },
  174. ],
  175. });
  176. let vercelCronsConfig = undefined;
  177. try {
  178. if (process.env.VERCEL && userSentryOptions.automaticVercelMonitors) {
  179. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  180. vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons;
  181. if (vercelCronsConfig) {
  182. logger.info(
  183. `${chalk.cyan(
  184. 'info',
  185. )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan(
  186. 'automaticVercelMonitors',
  187. )} option to false in you Next.js config.`,
  188. );
  189. }
  190. }
  191. } catch (e) {
  192. if ((e ).code === 'ENOENT') ; else {
  193. // log but noop
  194. logger.error(`${chalk.red('error')} - Sentry failed to read vercel.json`, e);
  195. }
  196. }
  197. // Wrap api routes
  198. newConfig.module.rules.unshift({
  199. test: isApiRouteResource,
  200. use: [
  201. {
  202. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  203. options: {
  204. ...staticWrappingLoaderOptions,
  205. vercelCronsConfig,
  206. wrappingTargetKind: 'api-route',
  207. },
  208. },
  209. ],
  210. });
  211. // Wrap middleware
  212. if (_nullishCoalesce(userSentryOptions.autoInstrumentMiddleware, () => ( true))) {
  213. newConfig.module.rules.unshift({
  214. test: isMiddlewareResource,
  215. use: [
  216. {
  217. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  218. options: {
  219. ...staticWrappingLoaderOptions,
  220. wrappingTargetKind: 'middleware',
  221. },
  222. },
  223. ],
  224. });
  225. }
  226. }
  227. if (isServer && userSentryOptions.autoInstrumentAppDirectory !== false) {
  228. // Wrap server components
  229. newConfig.module.rules.unshift({
  230. test: isServerComponentResource,
  231. use: [
  232. {
  233. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  234. options: {
  235. ...staticWrappingLoaderOptions,
  236. wrappingTargetKind: 'server-component',
  237. },
  238. },
  239. ],
  240. });
  241. // Wrap route handlers
  242. newConfig.module.rules.unshift({
  243. test: isRouteHandlerResource,
  244. use: [
  245. {
  246. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  247. options: {
  248. ...staticWrappingLoaderOptions,
  249. wrappingTargetKind: 'route-handler',
  250. },
  251. },
  252. ],
  253. });
  254. }
  255. if (isServer) {
  256. // Import the Sentry config in every user file
  257. newConfig.module.rules.unshift({
  258. test: resourcePath => {
  259. return (
  260. isPageResource(resourcePath) ||
  261. isApiRouteResource(resourcePath) ||
  262. isMiddlewareResource(resourcePath) ||
  263. isServerComponentResource(resourcePath) ||
  264. isRouteHandlerResource(resourcePath)
  265. );
  266. },
  267. use: [
  268. {
  269. loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
  270. options: {
  271. ...staticWrappingLoaderOptions,
  272. wrappingTargetKind: 'sentry-init',
  273. },
  274. },
  275. ],
  276. });
  277. }
  278. if (appDirPath) {
  279. const hasGlobalErrorFile = ['global-error.js', 'global-error.jsx', 'global-error.ts', 'global-error.tsx'].some(
  280. globalErrorFile => fs.existsSync(path.join(appDirPath, globalErrorFile)),
  281. );
  282. if (!hasGlobalErrorFile && !showedMissingGlobalErrorWarningMsg) {
  283. // eslint-disable-next-line no-console
  284. console.log(
  285. `${chalk.yellow(
  286. 'warn',
  287. )} - It seems like you don't have a global error handler set up. It is recommended that you add a ${chalk.cyan(
  288. 'global-error.js',
  289. )} file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router`,
  290. );
  291. showedMissingGlobalErrorWarningMsg = true;
  292. }
  293. }
  294. // The SDK uses syntax (ES6 and ES6+ features like object spread) which isn't supported by older browsers. For users
  295. // who want to support such browsers, `transpileClientSDK` allows them to force the SDK code to go through the same
  296. // transpilation that their code goes through. We don't turn this on by default because it increases bundle size
  297. // fairly massively.
  298. if (!isServer && _optionalChain([userSentryOptions, 'optionalAccess', _3 => _3.transpileClientSDK])) {
  299. // Find all loaders which apply transpilation to user code
  300. const transpilationRules = findTranspilationRules(_optionalChain([newConfig, 'access', _4 => _4.module, 'optionalAccess', _5 => _5.rules]), projectDir);
  301. // For each matching rule, wrap its `exclude` function so that it won't exclude SDK files, even though they're in
  302. // `node_modules` (which is otherwise excluded)
  303. transpilationRules.forEach(rule => {
  304. // All matching rules will necessarily have an `exclude` property, but this keeps TS happy
  305. if (rule.exclude && typeof rule.exclude === 'function') {
  306. const origExclude = rule.exclude;
  307. const newExclude = (filepath) => {
  308. if (filepath.includes('@sentry')) {
  309. // `false` in this case means "don't exclude it"
  310. return false;
  311. }
  312. return origExclude(filepath);
  313. };
  314. rule.exclude = newExclude;
  315. }
  316. });
  317. }
  318. // Tell webpack to inject user config files (containing the two `Sentry.init()` calls) into the appropriate output
  319. // bundles. Store a separate reference to the original `entry` value to avoid an infinite loop. (If we don't do
  320. // 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`.
  321. // Since we're setting `x.y` to be a callback (which, by definition, won't run until some time later), by the time
  322. // the function runs (causing `f` to run, causing `x.y` to run), `x.y` will point to the callback itself, rather
  323. // than its original value. So calling it will call the callback which will call `f` which will call `x.y` which
  324. // will call the callback which will call `f` which will call `x.y`... and on and on. Theoretically this could also
  325. // be fixed by using `bind`, but this is way simpler.)
  326. const origEntryProperty = newConfig.entry;
  327. newConfig.entry = async () => addSentryToEntryProperty(origEntryProperty, buildContext, userSentryOptions);
  328. // Enable the Sentry plugin (which uploads source maps to Sentry when not in dev) by default
  329. if (shouldEnableWebpackPlugin(buildContext, userSentryOptions)) {
  330. // TODO Handle possibility that user is using `SourceMapDevToolPlugin` (see
  331. // https://webpack.js.org/plugins/source-map-dev-tool-plugin/)
  332. // TODO (v9 or v10, maybe): Remove this
  333. handleSourcemapHidingOptionWarning(userSentryOptions, isServer);
  334. // Next doesn't let you change `devtool` in dev even if you want to, so don't bother trying - see
  335. // https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md
  336. if (!isDev) {
  337. // TODO (v8): Default `hideSourceMaps` to `true`
  338. // `hidden-source-map` produces the same sourcemaps as `source-map`, but doesn't include the `sourceMappingURL`
  339. // comment at the bottom. For folks who aren't publicly hosting their sourcemaps, this is helpful because then
  340. // the browser won't look for them and throw errors into the console when it can't find them. Because this is a
  341. // front-end-only problem, and because `sentry-cli` handles sourcemaps more reliably with the comment than
  342. // without, the option to use `hidden-source-map` only applies to the client-side build.
  343. newConfig.devtool = userSentryOptions.hideSourceMaps && !isServer ? 'hidden-source-map' : 'source-map';
  344. const SentryWebpackPlugin = loadModule('@sentry/webpack-plugin');
  345. if (SentryWebpackPlugin) {
  346. newConfig.plugins = newConfig.plugins || [];
  347. newConfig.plugins.push(new SentryCliDownloadPlugin());
  348. newConfig.plugins.push(
  349. // @ts-expect-error - this exists, the dynamic import just doesn't know about it
  350. new SentryWebpackPlugin(
  351. getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions, userSentryOptions),
  352. ),
  353. );
  354. }
  355. }
  356. }
  357. if (userSentryOptions.disableLogger) {
  358. newConfig.plugins = newConfig.plugins || [];
  359. newConfig.plugins.push(
  360. new buildContext.webpack.DefinePlugin({
  361. __SENTRY_DEBUG__: false,
  362. }),
  363. );
  364. }
  365. return newConfig;
  366. };
  367. }
  368. /**
  369. * Determine if this `module.rules` entry is one which will transpile user code
  370. *
  371. * @param rule The rule to check
  372. * @param projectDir The path to the user's project directory
  373. * @returns True if the rule transpiles user code, and false otherwise
  374. */
  375. function isMatchingRule(rule, projectDir) {
  376. // We want to run our SDK code through the same transformations the user's code will go through, so we test against a
  377. // sample user code path
  378. const samplePagePath = path.resolve(projectDir, 'pageFile.js');
  379. if (rule.test && rule.test instanceof RegExp && !rule.test.test(samplePagePath)) {
  380. return false;
  381. }
  382. if (Array.isArray(rule.include) && !rule.include.includes(projectDir)) {
  383. return false;
  384. }
  385. // `rule.use` can be an object or an array of objects. For simplicity, force it to be an array.
  386. const useEntries = arrayify(rule.use);
  387. // Depending on the version of nextjs we're talking about, the loader which does the transpiling is either
  388. //
  389. // 'next-babel-loader' (next 10),
  390. // '/abs/path/to/node_modules/next/more/path/babel/even/more/path/loader/yet/more/path/index.js' (next 11), or
  391. // 'next-swc-loader' (next 12).
  392. //
  393. // The next 11 option is ugly, but thankfully 'next', 'babel', and 'loader' do appear in it in the same order as in
  394. // 'next-babel-loader', so we can use the same regex to test for both.
  395. if (!useEntries.some(entry => _optionalChain([entry, 'optionalAccess', _6 => _6.loader]) && /next.*(babel|swc).*loader/.test(entry.loader))) {
  396. return false;
  397. }
  398. return true;
  399. }
  400. /**
  401. * Find all rules in `module.rules` which transpile user code.
  402. *
  403. * @param rules The `module.rules` value
  404. * @param projectDir The path to the user's project directory
  405. * @returns An array of matching rules
  406. */
  407. function findTranspilationRules(rules, projectDir) {
  408. if (!rules) {
  409. return [];
  410. }
  411. const matchingRules = [];
  412. // Each entry in `module.rules` is either a rule in and of itself or an object with a `oneOf` property, whose value is
  413. // an array of rules
  414. rules.forEach(rule => {
  415. // if (rule.oneOf) {
  416. if (isMatchingRule(rule, projectDir)) {
  417. matchingRules.push(rule);
  418. } else if (rule.oneOf) {
  419. const matchingOneOfRules = rule.oneOf.filter(oneOfRule => isMatchingRule(oneOfRule, projectDir));
  420. matchingRules.push(...matchingOneOfRules);
  421. // } else if (isMatchingRule(rule, projectDir)) {
  422. }
  423. });
  424. return matchingRules;
  425. }
  426. /**
  427. * Modify the webpack `entry` property so that the code in `sentry.server.config.js` and `sentry.client.config.js` is
  428. * included in the the necessary bundles.
  429. *
  430. * @param currentEntryProperty The value of the property before Sentry code has been injected
  431. * @param buildContext Object passed by nextjs containing metadata about the build
  432. * @returns The value which the new `entry` property (which will be a function) will return (TODO: this should return
  433. * the function, rather than the function's return value)
  434. */
  435. async function addSentryToEntryProperty(
  436. currentEntryProperty,
  437. buildContext,
  438. userSentryOptions,
  439. ) {
  440. // The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
  441. // sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
  442. // someone else has come along before us and changed that, we need to check a few things along the way. The one thing
  443. // we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
  444. // options. See https://webpack.js.org/configuration/entry-context/#entry.
  445. const { isServer, dir: projectDir, nextRuntime, dev: isDevMode } = buildContext;
  446. const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'node') : 'browser';
  447. const newEntryProperty =
  448. typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
  449. // `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents)
  450. const userConfigFile =
  451. nextRuntime === 'edge'
  452. ? getUserConfigFile(projectDir, 'edge')
  453. : isServer
  454. ? getUserConfigFile(projectDir, 'server')
  455. : getUserConfigFile(projectDir, 'client');
  456. // we need to turn the filename into a path so webpack can find it
  457. const filesToInject = userConfigFile ? [`./${userConfigFile}`] : [];
  458. // inject into all entry points which might contain user's code
  459. for (const entryPointName in newEntryProperty) {
  460. if (shouldAddSentryToEntryPoint(entryPointName, runtime)) {
  461. addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject, isDevMode);
  462. } else {
  463. if (
  464. isServer &&
  465. // If the user has asked to exclude pages, confirm for them that it's worked
  466. userSentryOptions.excludeServerRoutes &&
  467. // We always skip these, so it's not worth telling the user that we've done so
  468. !['pages/_app', 'pages/_document'].includes(entryPointName)
  469. ) {
  470. DEBUG_BUILD && logger.log(`Skipping Sentry injection for ${entryPointName.replace(/^pages/, '')}`);
  471. }
  472. }
  473. }
  474. return newEntryProperty;
  475. }
  476. /**
  477. * Search the project directory for a valid user config file for the given platform, allowing for it to be either a
  478. * TypeScript or JavaScript file.
  479. *
  480. * @param projectDir The root directory of the project, where the file should be located
  481. * @param platform Either "server", "client" or "edge", so that we know which file to look for
  482. * @returns The name of the relevant file. If the server or client file is not found, this method throws an error. The
  483. * edge file is optional, if it is not found this function will return `undefined`.
  484. */
  485. function getUserConfigFile(projectDir, platform) {
  486. const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`];
  487. for (const filename of possibilities) {
  488. if (fs.existsSync(path.resolve(projectDir, filename))) {
  489. return filename;
  490. }
  491. }
  492. // Edge config file is optional
  493. if (platform === 'edge') {
  494. // eslint-disable-next-line no-console
  495. console.warn(
  496. '[@sentry/nextjs] You are using Next.js features that run on the Edge Runtime. Please add a "sentry.edge.config.js" or a "sentry.edge.config.ts" file to your project root in which you initialize the Sentry SDK with "Sentry.init()".',
  497. );
  498. return;
  499. } else {
  500. throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`);
  501. }
  502. }
  503. /**
  504. * Gets the absolute path to a sentry config file for a particular platform. Returns `undefined` if it doesn't exist.
  505. */
  506. function getUserConfigFilePath(projectDir, platform) {
  507. const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`];
  508. for (const filename of possibilities) {
  509. const configPath = path.resolve(projectDir, filename);
  510. if (fs.existsSync(configPath)) {
  511. return configPath;
  512. }
  513. }
  514. return undefined;
  515. }
  516. /**
  517. * Add files to a specific element of the given `entry` webpack config property.
  518. *
  519. * @param entryProperty The existing `entry` config object
  520. * @param entryPointName The key where the file should be injected
  521. * @param filesToInsert An array of paths to the injected files
  522. */
  523. function addFilesToExistingEntryPoint(
  524. entryProperty,
  525. entryPointName,
  526. filesToInsert,
  527. isDevMode,
  528. ) {
  529. // BIG FAT NOTE: Order of insertion seems to matter here. If we insert the new files before the `currentEntrypoint`s,
  530. // the Next.js dev server breaks. Because we generally still want the SDK to be initialized as early as possible we
  531. // still keep it at the start of the entrypoints if we are not in dev mode.
  532. // can be a string, array of strings, or object whose `import` property is one of those two
  533. const currentEntryPoint = entryProperty[entryPointName];
  534. let newEntryPoint = currentEntryPoint;
  535. if (typeof currentEntryPoint === 'string' || Array.isArray(currentEntryPoint)) {
  536. newEntryPoint = arrayify(currentEntryPoint);
  537. if (newEntryPoint.some(entry => filesToInsert.includes(entry))) {
  538. return;
  539. }
  540. if (isDevMode) {
  541. // Inserting at beginning breaks dev mode so we insert at the end
  542. newEntryPoint.push(...filesToInsert);
  543. } else {
  544. // In other modes we insert at the beginning so that the SDK initializes as early as possible
  545. newEntryPoint.unshift(...filesToInsert);
  546. }
  547. }
  548. // descriptor object (webpack 5+)
  549. else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) {
  550. const currentImportValue = currentEntryPoint.import;
  551. const newImportValue = arrayify(currentImportValue);
  552. if (newImportValue.some(entry => filesToInsert.includes(entry))) {
  553. return;
  554. }
  555. if (isDevMode) {
  556. // Inserting at beginning breaks dev mode so we insert at the end
  557. newImportValue.push(...filesToInsert);
  558. } else {
  559. // In other modes we insert at the beginning so that the SDK initializes as early as possible
  560. newImportValue.unshift(...filesToInsert);
  561. }
  562. newEntryPoint = {
  563. ...currentEntryPoint,
  564. import: newImportValue,
  565. };
  566. }
  567. // malformed entry point (use `console.error` rather than `logger.error` because it will always be printed, regardless
  568. // of SDK settings)
  569. else {
  570. // eslint-disable-next-line no-console
  571. console.error(
  572. 'Sentry Logger [Error]:',
  573. `Could not inject SDK initialization code into entry point ${entryPointName}, as its current value is not in a recognized format.\n`,
  574. 'Expected: string | Array<string> | { [key:string]: any, import: string | Array<string> }\n',
  575. `Got: ${currentEntryPoint}`,
  576. );
  577. }
  578. entryProperty[entryPointName] = newEntryPoint;
  579. }
  580. /**
  581. * Check the SentryWebpackPlugin options provided by the user against the options we set by default, and warn if any of
  582. * our default options are getting overridden. (Note: If any of our default values is undefined, it won't be included in
  583. * the warning.)
  584. *
  585. * @param defaultOptions Default SentryWebpackPlugin options
  586. * @param userOptions The user's SentryWebpackPlugin options
  587. */
  588. function checkWebpackPluginOverrides(
  589. defaultOptions,
  590. userOptions,
  591. ) {
  592. // warn if any of the default options for the webpack plugin are getting overridden
  593. const sentryWebpackPluginOptionOverrides = Object.keys(defaultOptions).filter(key => key in userOptions);
  594. if (sentryWebpackPluginOptionOverrides.length > 0) {
  595. DEBUG_BUILD &&
  596. logger.warn(
  597. '[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
  598. `\t${sentryWebpackPluginOptionOverrides.toString()},\n` +
  599. "which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing.",
  600. );
  601. }
  602. }
  603. /**
  604. * Determine if this is an entry point into which both `Sentry.init()` code and the release value should be injected
  605. *
  606. * @param entryPointName The name of the entry point in question
  607. * @param isServer Whether or not this function is being called in the context of a server build
  608. * @param excludeServerRoutes A list of excluded serverside entrypoints provided by the user
  609. * @returns `true` if sentry code should be injected, and `false` otherwise
  610. */
  611. function shouldAddSentryToEntryPoint(entryPointName, runtime) {
  612. return (
  613. runtime === 'browser' &&
  614. (entryPointName === 'pages/_app' ||
  615. // entrypoint for `/app` pages
  616. entryPointName === 'main-app')
  617. );
  618. }
  619. /**
  620. * Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or
  621. * client files.
  622. *
  623. * @param buildContext Nexjs-provided data about the current build
  624. * @param userPluginOptions User-provided SentryWebpackPlugin options
  625. * @returns Final set of combined options
  626. */
  627. function getWebpackPluginOptions(
  628. buildContext,
  629. userPluginOptions,
  630. userSentryOptions,
  631. ) {
  632. const { buildId, isServer, config, dir: projectDir } = buildContext;
  633. const userNextConfig = config ;
  634. const distDirAbsPath = path.resolve(projectDir, userNextConfig.distDir || '.next'); // `.next` is the default directory
  635. const isServerless = userNextConfig.target === 'experimental-serverless-trace';
  636. const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties'));
  637. const urlPrefix = '~/_next';
  638. const serverInclude = isServerless
  639. ? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }]
  640. : [{ paths: [`${distDirAbsPath}/server/`], urlPrefix: `${urlPrefix}/server` }];
  641. const serverIgnore = [];
  642. const clientInclude = userSentryOptions.widenClientFileUpload
  643. ? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }]
  644. : [
  645. { paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` },
  646. { paths: [`${distDirAbsPath}/static/chunks/app`], urlPrefix: `${urlPrefix}/static/chunks/app` },
  647. ];
  648. // Widening the upload scope is necessarily going to lead to us uploading files we don't need to (ones which
  649. // don't include any user code). In order to lessen that where we can, exclude the internal nextjs files we know
  650. // will be there.
  651. const clientIgnore = userSentryOptions.widenClientFileUpload
  652. ? ['framework-*', 'framework.*', 'main-*', 'polyfills-*', 'webpack-*']
  653. : [];
  654. const defaultPluginOptions = dropUndefinedKeys({
  655. include: isServer ? serverInclude : clientInclude,
  656. ignore: isServer ? serverIgnore : clientIgnore,
  657. url: process.env.SENTRY_URL,
  658. org: process.env.SENTRY_ORG,
  659. project: process.env.SENTRY_PROJECT,
  660. authToken: process.env.SENTRY_AUTH_TOKEN,
  661. configFile: hasSentryProperties ? 'sentry.properties' : undefined,
  662. stripPrefix: ['webpack://_N_E/', 'webpack://'],
  663. urlPrefix,
  664. entries: [], // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
  665. release: getSentryRelease(buildId),
  666. });
  667. checkWebpackPluginOverrides(defaultPluginOptions, userPluginOptions);
  668. return {
  669. ...defaultPluginOptions,
  670. ...userPluginOptions,
  671. errorHandler(err, invokeErr, compilation) {
  672. if (err) {
  673. const errorMessagePrefix = `${chalk.red('error')} -`;
  674. if (err.message.includes('ENOENT')) {
  675. if (!showedMissingCliBinaryWarningMsg) {
  676. // eslint-disable-next-line no-console
  677. console.error(
  678. `\n${errorMessagePrefix} ${chalk.bold(
  679. 'The Sentry binary to upload sourcemaps could not be found.',
  680. )} Source maps will not be uploaded. Please check that post-install scripts are enabled in your package manager when installing your dependencies and please run your build once without any caching to avoid caching issues of dependencies.\n`,
  681. );
  682. showedMissingCliBinaryWarningMsg = true;
  683. }
  684. return;
  685. }
  686. // Hardcoded way to check for missing auth token until we have a better way of doing this.
  687. if (err.message.includes('Authentication credentials were not provided.')) {
  688. let msg;
  689. if (process.env.VERCEL) {
  690. msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan(
  691. 'SENTRY_AUTH_TOKEN',
  692. )} environment variable: https://vercel.com/integrations/sentry`;
  693. } else {
  694. msg =
  695. 'You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/\n' +
  696. `After generating a Sentry auth token, set it via the ${chalk.bold.cyan(
  697. 'SENTRY_AUTH_TOKEN',
  698. )} environment variable during the build.`;
  699. }
  700. if (!showedMissingAuthTokenErrorMsg) {
  701. // eslint-disable-next-line no-console
  702. console.error(
  703. `${errorMessagePrefix} ${chalk.bold(
  704. 'No Sentry auth token configured.',
  705. )} Source maps will not be uploaded.\n${msg}\n`,
  706. );
  707. showedMissingAuthTokenErrorMsg = true;
  708. }
  709. return;
  710. }
  711. // Hardcoded way to check for missing org slug until we have a better way of doing this.
  712. if (err.message.includes('An organization slug is required')) {
  713. let msg;
  714. if (process.env.VERCEL) {
  715. msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan(
  716. 'SENTRY_ORG',
  717. )} environment variable: https://vercel.com/integrations/sentry`;
  718. } else {
  719. msg = `To fix this, set the ${chalk.bold.cyan(
  720. 'SENTRY_ORG',
  721. )} environment variable to the to your organization slug during the build.`;
  722. }
  723. if (!showedMissingOrgSlugErrorMsg) {
  724. // eslint-disable-next-line no-console
  725. console.error(
  726. `${errorMessagePrefix} ${chalk.bold(
  727. 'No Sentry organization slug configured.',
  728. )} Source maps will not be uploaded.\n${msg}\n`,
  729. );
  730. showedMissingOrgSlugErrorMsg = true;
  731. }
  732. return;
  733. }
  734. // Hardcoded way to check for missing project slug until we have a better way of doing this.
  735. if (err.message.includes('A project slug is required')) {
  736. let msg;
  737. if (process.env.VERCEL) {
  738. msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan(
  739. 'SENTRY_PROJECT',
  740. )} environment variable: https://vercel.com/integrations/sentry`;
  741. } else {
  742. msg = `To fix this, set the ${chalk.bold.cyan(
  743. 'SENTRY_PROJECT',
  744. )} environment variable to the name of your Sentry project during the build.`;
  745. }
  746. if (!showedMissingProjectSlugErrorMsg) {
  747. // eslint-disable-next-line no-console
  748. console.error(
  749. `${errorMessagePrefix} ${chalk.bold(
  750. 'No Sentry project slug configured.',
  751. )} Source maps will not be uploaded.\n${msg}\n`,
  752. );
  753. showedMissingProjectSlugErrorMsg = true;
  754. }
  755. return;
  756. }
  757. }
  758. if (userPluginOptions.errorHandler) {
  759. return userPluginOptions.errorHandler(err, invokeErr, compilation);
  760. }
  761. return invokeErr();
  762. },
  763. };
  764. }
  765. /** Check various conditions to decide if we should run the plugin */
  766. function shouldEnableWebpackPlugin(buildContext, userSentryOptions) {
  767. const { isServer } = buildContext;
  768. const { disableServerWebpackPlugin, disableClientWebpackPlugin } = userSentryOptions;
  769. if (isServer && disableServerWebpackPlugin !== undefined) {
  770. return !disableServerWebpackPlugin;
  771. } else if (!isServer && disableClientWebpackPlugin !== undefined) {
  772. return !disableClientWebpackPlugin;
  773. }
  774. return true;
  775. }
  776. /** Handle warning messages about `hideSourceMaps` option. Can be removed in v9 or v10 (or whenever we consider that
  777. * enough people will have upgraded the SDK that the warning about the default in v8 - currently commented out - is
  778. * overkill). */
  779. function handleSourcemapHidingOptionWarning(userSentryOptions, isServer) {
  780. // This is nextjs's own logging formatting, vendored since it's not exported. See
  781. // https://github.com/vercel/next.js/blob/c3ceeb03abb1b262032bd96457e224497d3bbcef/packages/next/build/output/log.ts#L3-L11
  782. // and
  783. // https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/lib/eslint/runLintCheck.ts#L321-L323.
  784. const codeFormat = (str) => chalk.bold.cyan(str);
  785. const _warningPrefix_ = `${chalk.yellow('warn')} -`;
  786. const _sentryNextjs_ = codeFormat('@sentry/nextjs');
  787. const _hideSourceMaps_ = codeFormat('hideSourceMaps');
  788. const _true_ = codeFormat('true');
  789. const _false_ = codeFormat('false');
  790. const _sentry_ = codeFormat('sentry');
  791. const _nextConfigJS_ = codeFormat('next.config.js');
  792. if (isServer && userSentryOptions.hideSourceMaps === undefined && !showedHiddenSourceMapsWarningMsg) {
  793. // eslint-disable-next-line no-console
  794. console.warn(
  795. `\n${_warningPrefix_} In order to be able to deminify errors, ${_sentryNextjs_} creates sourcemaps and uploads ` +
  796. 'them to the Sentry server. Depending on your deployment setup, this means your original code may be visible ' +
  797. `in browser devtools in production. To prevent this, set ${_hideSourceMaps_} to ${_true_} in the ${_sentry_} ` +
  798. `options in your ${_nextConfigJS_}. To disable this warning without changing sourcemap behavior, set ` +
  799. `${_hideSourceMaps_} to ${_false_}. (In ${_sentryNextjs_} version 8.0.0 and beyond, this option will default ` +
  800. `to ${_true_}.) See https://webpack.js.org/configuration/devtool/ and ` +
  801. 'https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map for more ' +
  802. 'information.\n',
  803. );
  804. showedHiddenSourceMapsWarningMsg = true;
  805. }
  806. // TODO (v8): Remove the check above in favor of the one below
  807. // const infoPrefix = `${chalk.cyan('info')} -`;
  808. //
  809. // if (isServer && userSentryOptions.hideSourceMaps === true) {
  810. // // eslint-disable-next-line no-console
  811. // console.log(
  812. // `\n${infoPrefix} Starting in ${_sentryNextjs_} version 8.0.0, ${_hideSourceMaps_} defaults to ${_true_}, and ` +
  813. // `thus can be removed from the ${_sentry_} options in ${_nextConfigJS_}. See ` +
  814. // 'https://webpack.js.org/configuration/devtool/ and ' +
  815. // 'https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map for more ' +
  816. // 'information.\n',
  817. // );
  818. // }
  819. }
  820. /**
  821. * Ensure that `newConfig.module.rules` exists. Modifies the given config in place but also returns it in order to
  822. * change its type.
  823. *
  824. * @param newConfig A webpack config object which may or may not contain `module` and `module.rules`
  825. * @returns The same object, with an empty `module.rules` array added if necessary
  826. */
  827. function setUpModuleRules(newConfig) {
  828. newConfig.module = {
  829. ...newConfig.module,
  830. rules: [...(_optionalChain([newConfig, 'access', _7 => _7.module, 'optionalAccess', _8 => _8.rules]) || [])],
  831. };
  832. // Surprising that we have to assert the type here, since we've demonstrably guaranteed the existence of
  833. // `newConfig.module.rules` just above, but ¯\_(ツ)_/¯
  834. return newConfig ;
  835. }
  836. /**
  837. * Adds loaders to inject values on the global object based on user configuration.
  838. */
  839. function addValueInjectionLoader(
  840. newConfig,
  841. userNextConfig,
  842. userSentryOptions,
  843. buildContext,
  844. sentryWebpackPluginOptions,
  845. ) {
  846. const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
  847. const isomorphicValues = {
  848. // `rewritesTunnel` set by the user in Next.js config
  849. __sentryRewritesTunnelPath__:
  850. userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export'
  851. ? `${_nullishCoalesce(userNextConfig.basePath, () => ( ''))}${userSentryOptions.tunnelRoute}`
  852. : undefined,
  853. // The webpack plugin's release injection breaks the `app` directory so we inject the release manually here instead.
  854. // Having a release defined in dev-mode spams releases in Sentry so we only set one in non-dev mode
  855. SENTRY_RELEASE: buildContext.dev
  856. ? undefined
  857. : { id: _nullishCoalesce(sentryWebpackPluginOptions.release, () => ( getSentryRelease(buildContext.buildId))) },
  858. __sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
  859. };
  860. const serverValues = {
  861. ...isomorphicValues,
  862. // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
  863. // characters)
  864. __rewriteFramesDistDir__: _optionalChain([userNextConfig, 'access', _9 => _9.distDir, 'optionalAccess', _10 => _10.replace, 'call', _11 => _11(/\\/g, '\\\\')]) || '.next',
  865. };
  866. const clientValues = {
  867. ...isomorphicValues,
  868. // Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
  869. // `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
  870. __rewriteFramesAssetPrefixPath__: assetPrefix
  871. ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
  872. : '',
  873. };
  874. newConfig.module.rules.push(
  875. {
  876. test: /sentry\.(server|edge)\.config\.(jsx?|tsx?)/,
  877. use: [
  878. {
  879. loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
  880. options: {
  881. values: serverValues,
  882. },
  883. },
  884. ],
  885. },
  886. {
  887. test: /sentry\.client\.config\.(jsx?|tsx?)/,
  888. use: [
  889. {
  890. loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'),
  891. options: {
  892. values: clientValues,
  893. },
  894. },
  895. ],
  896. },
  897. );
  898. }
  899. function resolveNextPackageDirFromDirectory(basedir) {
  900. try {
  901. return path.dirname(sync('next/package.json', { basedir }));
  902. } catch (e2) {
  903. // Should not happen in theory
  904. return undefined;
  905. }
  906. }
  907. const POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS = [
  908. // Original location of RequestAsyncStorage
  909. // https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts
  910. 'next/dist/client/components/request-async-storage.js',
  911. // Introduced in Next.js 13.4.20
  912. // https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts
  913. 'next/dist/client/components/request-async-storage.external.js',
  914. ];
  915. function getRequestAsyncStorageModuleLocation(
  916. webpackContextDir,
  917. webpackResolvableModuleLocations,
  918. ) {
  919. if (webpackResolvableModuleLocations === undefined) {
  920. return undefined;
  921. }
  922. const absoluteWebpackResolvableModuleLocations = webpackResolvableModuleLocations.map(loc =>
  923. path.resolve(webpackContextDir, loc),
  924. );
  925. for (const webpackResolvableLocation of absoluteWebpackResolvableModuleLocations) {
  926. const nextPackageDir = resolveNextPackageDirFromDirectory(webpackResolvableLocation);
  927. if (nextPackageDir) {
  928. const asyncLocalStorageLocation = POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS.find(loc =>
  929. fs.existsSync(path.join(nextPackageDir, '..', loc)),
  930. );
  931. if (asyncLocalStorageLocation) {
  932. return asyncLocalStorageLocation;
  933. }
  934. }
  935. }
  936. return undefined;
  937. }
  938. let downloadingCliAttempted = false;
  939. class SentryCliDownloadPlugin {
  940. apply(compiler) {
  941. compiler.hooks.beforeRun.tapAsync('SentryCliDownloadPlugin', (compiler, callback) => {
  942. const SentryWebpackPlugin = loadModule('@sentry/webpack-plugin');
  943. if (!SentryWebpackPlugin) {
  944. // Pretty much an invariant.
  945. return callback();
  946. }
  947. // @ts-expect-error - this exists, the dynamic import just doesn't know it
  948. if (SentryWebpackPlugin.cliBinaryExists()) {
  949. return callback();
  950. }
  951. if (!downloadingCliAttempted) {
  952. downloadingCliAttempted = true;
  953. // eslint-disable-next-line no-console
  954. logger.info(
  955. `\n${chalk.cyan('info')} - ${chalk.bold(
  956. 'Sentry binary to upload source maps not found.',
  957. )} Package manager post-install scripts are likely disabled or there is a caching issue. Manually downloading instead...`,
  958. );
  959. // @ts-expect-error - this exists, the dynamic import just doesn't know it
  960. const cliDownloadPromise = SentryWebpackPlugin.downloadCliBinary({
  961. log: () => {
  962. // No logs from directly from CLI
  963. },
  964. });
  965. cliDownloadPromise.then(
  966. () => {
  967. // eslint-disable-next-line no-console
  968. logger.info(`${chalk.cyan('info')} - Sentry binary was successfully downloaded.\n`);
  969. return callback();
  970. },
  971. e => {
  972. // eslint-disable-next-line no-console
  973. logger.error(`${chalk.red('error')} - Sentry binary download failed:`, e);
  974. return callback();
  975. },
  976. );
  977. } else {
  978. return callback();
  979. }
  980. });
  981. }
  982. }
  983. export { constructWebpackConfigFunction, getUserConfigFile, getUserConfigFilePath, getWebpackPluginOptions };
  984. //# sourceMappingURL=webpack.js.map