webpack.js 45 KB

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